Будь умным!


У вас вопросы?
У нас ответы:) SamZan.net

ПРАКТИКУМ ОСНОВЫ ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ ВЫСОКОГО УРОВНЯ C

Работа добавлена на сайт samzan.net: 2016-06-20

Поможем написать учебную работу

Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.

Предоплата всего

от 25%

Подписываем

договор

Выберите тип работы:

Скидка 25% при заказе до 22.5.2024

Министерство образования и науки  Российской Федерации

Федеральное агентство по образованию

Ставропольский институт управления

Воронкин Р.А., Банько М.А.

ЛАБОРАТОРНЫЙ ПРАКТИКУМ

ОСНОВЫ ПРОГРАММИРОВАНИЯ

НА ЯЗЫКЕ ВЫСОКОГО УРОВНЯ C++

Ставрополь, 2007

Лабораторный практикум «Основы программирования на языке C++» предназначен для студентов, обучающихся на специальностях 230102 «Автоматизированные системы обработки информации и управления» и 080801 «Прикладная информатика в экономике».

Данный лабораторный практикум позволяет получить основные практические навыки разработки консольных приложений на языке C++ с помощью Microsoft Visual Studio, ознакомиться с принципами структурного программирования, нашедшими реализацию в языке C++, и основами объектно-ориентированного программирования. И может быть использован в качестве основы для выполнения лабораторных работ по дисциплинам «Высокоуровневые методы информатики и программирования» и «Объектно-ориентированное программирование».

Составители:

к. т. н. Р.А. Воронкин

к. ф.-м. н. М.А. Банько

Рецензент:

к. ф.-м. н. А.С. Ионисян

СОДЕРЖАНИЕ

[1] Предисловие

[2] Лабораторная работа №1.
Интегрированная среда разработки Microsoft Visual Studio.
Программирование алгоритмов линейной структуры

[3] Лабораторная работа №2.
Программирование алгоритмов разветвляющейся структуры
в языке  C++

[4]
Лабораторная работа №3.
Организация циклических вычислений в языке  C++

[5] Лабораторная работа №4.
Одномерные массивы в языке C++

[6]
Лабораторная работа №5.
Указатели и ссылки  в языке C++

[7] Лабораторная работа №6.
Двумерные массивы в языке C++

[8] Лабораторная работа №7.
Функции и перегрузка в языке C++

[9] Лабораторная работа №8.
Строки и файлы в языке C++

[10] Лабораторная работа №9.
Структуры в языке C++

[11] Лабораторная работа №10.
Классы в языке C++

[12] Список рекомендуемой литературы

Предисловие

Данный лабораторный практикум предназначен для привития студентам навыков и умений разработки консольных приложений на языке C++ с использованием структурного и объектно-ориентированного подходов.

Основная цель лабораторного практикума - помочь студентам закрепить знания, полученные на лекционных занятиях и овладеть навыками структурного и объектно-ориентированного программирования при разработке приложений на языке C++ в среде Microsoft Visual Studio.

В процессе выполнения заданий студенты рассмотрят процесс создания модульных программ, элементы теории модульного программирования, объектно-ориентированный подход к проектированию и разработке программ, объектные типы данных и переменные объектного типа, основные принципы объектно-ориентированного программирования (инкапсуляцию, наследование, полиморфизм), а также освоят интерфейс среды разработки приложений Microsoft Visual C++.NET. Вопросы, изученные в ходе выполнения  лабораторных работ, помогут студентам освоить основные принципы разработки приложений на объектно-ориентированном языке C++, что будет являться основой для дальнейшего изучения как языков и технологий программирования, так и систем управления базами данных.

Лабораторная работа №1.
Интегрированная среда разработки Microsoft Visual Studio.
Программирование алгоритмов линейной структуры

Цель работы и содержание: изучение возможностей среды разработки Microsoft Visual Studio, разработка консольных приложений в среде Microsoft Visual Studio, приобретение навыков программирования линейных процессов, основы отладки программ в среде Microsoft Visual Studio. Освоить технологию программирования линейных алгоритмов на языке C++, функции ввода/вывода данных, оператора присваивания.

Ход работы

Основные сведения о языке C++. Язык C++ является универсальным языком программирования, в дополнение к которому разработан набор разнообразных библиотек. Поэтому он позволяет решить практически любую задачу программирования.

Язык программирования C++(читается «си плюс плюс») был разработан на основе языка C(Си) Бьярном Страуструпом в 1972 году. На первых этапах разработки язык носил условное название «Си с классами», а позже Рик Маскитти придумал название «C++», что образно отразило происхождение этого нового языка от языка C. Язык C++ является расширением языка C, поэтому программы, написанные на языке C, могут обрабатываться компилятором языка C++. Язык C++ был создан, чтобы улучшить язык C, поддержать абстракцию данных и обеспечить объектно-ориентированное программирование (ООП).

С++ - это язык программирования общего назначения, хорошо известный своей эффективностью, экономичностью, и переносимостью. Указанные преимущества C++ обеспечивают хорошее качество разработки почти любого вида программного продукта. Использование C++ в качестве инструментального языка позволяет получать быстрые и компактные программы. Во многих случаях программы, написанные на C++, сравнимы по скорости с программами, написанными на языке Ассемблера.

Основные элементы среды разработки Microsoft Visual Studio. Integrated Development Environment (интегрированная среда разработки), или, сокращенно, IDE - это программный продукт, объединяющий текстовый редактор, компилятор, отладчик и справочную систему.

Любая программа, создаваемая в среде Visual C++.NET, даже такая простая, как «Hello, World!», всегда оформляется как отдельный проект (project). Проект - это набор взаимосвязанных исходных файлов и, возможно, включаемых (заголовочных) файлов, компиляция и компоновка которых позволяет создать исполняемую программу. Рабочая область может содержать любое количество различных проектов, сгруппированных вместе для согласованной разработки: от отдельного приложения до библиотеки функций или целого программного пакета. Очевидно, что для решения наших учебных задач каждая программа будет воплощаться в виде одного проекта, поэтому рабочая область проекта у нас всегда будет содержать ровно один проект.

Запуск IDE. Типы приложений. Вызов Visual C++.NET осуществляется или через меню Пуск ► Программы ► Microsoft Visual StudioMicrosoft Visual Studio, или щелчком мышью по пиктограмме с соответствующим именем.

После запуска Microsoft Visual Studio появляется главное окно программы, показанное на рисунке 1.1. (В зависимости от настроек для вашего рабочего стола Microsoft Visual Studio его вид может несколько отличаться от показанного на рисунке 1.1.

Рисунок 1.1 - IDE Microsoft Visual Studio

На самом деле главное окно (рабочий стол) Visual C++ принадлежит студии разработчика Microsoft Developer Studio - интегрированной среде, общей для Visual C++, Visual J#, Visual Basic, Visual C# и некоторых других продуктов. Рабочий стол Visual C++ включает в себя три окна.

  1.  Окно Project Workspace (окно рабочей области) предназначено для оказания помощи при написании и сопровождении больших многофайловых программ. Пока что оно закрыто, но после создания нового проекта (или загрузки сохраненного ранее проекта) одна из вкладок этого окна будет содержать список файлов проекта.
  2.  Окно Editor (окно редактора) используется для ввода и проверки исходного кода.
  3.  Окно Output (окно вывода) служит для вывода сообщений о ходе компиляции, сборки и выполнения программы. В частности, сообщения о возникающих ошибках появляются именно в этом окне.

Под заголовком главного окна, как и во всех Windows-приложениях, находится строка меню. Назначение команд меню и кнопок панелей инструментов мы будем рассматривать по мере необходимости, разбирая основные приемы работы в IDE. Для кнопок панелей инструментов предусмотрена удобная контекстная помощь: если навести курсор мыши на кнопку и задержать, то всплывет подсказка с назначением кнопки.

Microsoft Developer Studio позволяет строить проекты разных типов, ориентированные на различные сферы применения. Так как эта студия спроектирована на Windows-платформе, то почти все типы проектов являются оконными Windows-приложениями с соответствующим графическим интерфейсом. В то же время разработчики Microsoft Developer Studio предусмотрели работу и с так называемыми консольными приложениями. При запуске консольного приложения операционная система создает так называемое консольное окно, через которое идет весь ввод-вывод программы. Внешне это напоминает работу в операционной системе MS DOS или других операционных системах в режиме командной строки.

Создание нового проекта. Для создания нового проекта типа «консольное приложение» выполните следующие действия:

  •  выберите в строке меню главного окна команду FileNew...
  •  в открывшемся диалоговом окне New выберите вкладку Projects:

Рисунок 1.2 - Создание консольного проекта С++ в Microsoft Visual Studio

  •  выберите тип Win32 Console Application;
  •  введите имя проекта в текстовом поле Project Name, например First;
  •  введите имя каталога размещения файлов проекта в текстовом поле Location (если указанный вами каталог отсутствует, то он будет создан автоматически);
  •  щелкните левой кнопкой мыши на кнопке ОК.

Рисунок 1.3 - Окно приветствия при создании консольного проекта
в Visual C++

  •  После щелчка запускается так называемый мастер приложений Application Wizard, который открывает диалоговое окно Win32 Console Application с предложением определиться, какой подтип консольного приложения вам требуется создать.
  •  Установите опции проекта согласно рисунка 1.4.
  •  Щелкните на кнопке Finish.

Рисунок 1.4 - Опции консольного проекта Microsoft Visual C++

  •  После щелчка появится окно New Project Information со спецификациями проекта и информацией о каталоге, в котором будет размещен создаваемый проект. Щелкните на кнопке ОК.

Допустим, что в качестве Project Name вы ввели имя First. Тогда после выполненных шагов вы увидите на экране примерно то, что показано на рисунке 1.5.

Рисунок 1.5 - Результат работы мастера создания консольного проекта
в
Visual C++

Прежде чем продолжать работу, свернем временно главное окно Visual C++ на панель задач и заглянем в папку First, созданную мастером приложений для нашего проекта, а точнее - для нашей рабочей области. Там находятся файлы First.dsw, First.dsp, First.opt, First.ncb, а также папку Debug (или Release - в зависимости от конфигурации проекта). Дадим краткое описание каждого из файлов:

  •  First.dsw - файл рабочей области проекта, используемый внутри интегрированной среды разработки. Он объединяет всю информацию о проектах, входящих в данную рабочую область.
  •  First.dsp - проектный файл, используемый для построения (building) отдельного проекта или подпроекта.
  •  First.opt - файл, содержащий опции рабочей области проекта. Благодаря этому файлу при каждом открытии рабочей области проекта все параметры Developer Studio, выбранные во время последнего сеанса работы с данной рабочей областью, будут восстановлены.
  •  First.ncb - служебный файл. Он создается компилятором и содержит информацию, которая используется в инструменте интегрированной среды под названием ClassView. Панель ClassView находится в окне Project Workspace и показывает все классы C++, для которых доступны определения в рамках данного проекта, а также все элементы этих классов.
  •  Debug - папка, в которую будут помещаться файлы, формируемые компилятором и сборщиком. Из этих файлов нас будет интересовать, в общем-то, только один - исполняемый файл, имеющий расширение .ехе.

Развернем главное окно Visual C++ с открытой рабочей областью First, чтобы продолжить работу по созданию нашей первой программы. Первое, что бросается в глаза - окно Project Workspace «оживилось», в нем появились две вкладки: Class View и File View. С вкладкой Class View в ближайшее время мы работать не будем, поэтому щелчком мыши переключимся на вкладку File View. Она предназначена для просмотра списка файлов проекта. Откроем список First files, щелкнув мышью на значке «+». Появится дерево списка файлов, содержащее пиктограммы трех папок: Source Files, Header Files, Resource Files. Так как в консольных приложениях файлы ресурсов не используются, то про последнюю папку сразу забудем. Попробуем заглянуть (щелчком мыши) в первые две папки. Попытка окажется неудачной - папки пусты. Это и неудивительно: ведь мы выбрали в качестве подтипа консольного приложения пустой проект - опцию An empty project. Так что наполнение проекта конкретным содержанием у нас еще впереди.

Добавление к проекту файлов с исходным кодом. Рассмотрим две ситуации:

- добавление существующего файла,

- создание нового файла.

Добавление существующего файла. В этом случае файл с исходным кодом (пусть это будет файл first.cpp) уже подготовлен ранее в каком-то текстовом редакторе. Продолжение должно быть следующим:

  •  Скопируйте исходный файл (first.cpp) в папку рабочей области проекта (в данном случае - First).
  •  Вернитесь к списку First files в окне Project Workspace вашего проекта и щелкните правой кнопкой мыши на папке Source Files.
  •  В появившемся контекстном меню щелчком мыши выберите команду добавления файлов Add Files to Folder....
  •  В открывшемся диалоговом окне Insert Files... выберите нужный файл (first.cpp) и щелкните на кнопке ОК.

 Добавление нового файла. В этом случае необходимо выполнить следующие действия:

  •  Выберите в строке меню главного окна команду File ► New.... В результате откроется диалоговое окно New.
  •  На вкладке Files:
    1.  выберите тип файла (в данном случае: C++ Source File);
    2.  в текстовом поле File Name введите нужное имя файла (в данном случае: first.cpp);
    3.  флажок Add to project должен быть включен;
    4.  щелкните на кнопке ОК.

Многофайловые проекты. В многофайловых проектах обычно присутствуют заголовочные файлы - они создаются/добавляются после щелчка правой кнопкой мыши на пиктограмме папки Header Files в окне Project Workspace; при этом на вкладке Files диалогового окна New выбирается тип файла C/C++ Header File, а при вводе имени файла используется расширение .h.

Папки Source Files и Header Files, пиктограммы которых вы видите в окне Project Workspace, на самом деле физически не существуют, то есть все файлы помещаются в основную папку рабочей области проекта, имя которой было задано при создании проекта в окне Project Name.

Компиляция, компоновка и выполнение проекта. Эти операции могут быть выполнены или через меню Build главного окна. Опишем кратко основные команды меню Build:

- Compile - компиляция выбранного файла. Результаты компиляции выводятся в окно Output.

- Build - компоновка проекта. Компилируются все файлы, в которых произошли изменения с момента последней компоновки. После компиляции происходит сборка (link) всех объектных модулей, включая библиотечные, в результирующий исполняемый файл. Сообщения об ошибках компоновки выводятся в окно Output. Если обе фазы компоновки завершились без ошибок, то созданный исполняемый файл с расширением .ехе может быть запущен на выполнение.

- Rebuild All - то же, что и Build, но компилируются все файлы проекта независимо от того, были ли в них произведены изменения.

Если при компоновке многофайлового проекта посредством команды Build вы получаете сообщения об ошибках компиляции или сборки, которые вы не можете объяснить, то мы настоятельно рекомендуем вам воспользоваться командой Rebuild All.

- Execute - выполнение исполняемого файла, созданного в результате компоновки проекта.

Проблемы с вводом-выводом кириллицы. Работа в среде Visual C++ (в режиме консольных приложений) сопряжена с определенными неудобствами, вызванными различными стандартами кодировки символов кириллицы в операционных системах MS DOS и Windows. Весь ввод-вывод в консольном окне идет в кодировке стандарта ASCII, а текст в исходных файлах, набираемый в текстовом редакторе Visual C++, имеет кодировку в стандарте ANSI. Существует несколько способов нормального вывода строки, содержащей буквы русского алфавита:

1. Эту строку надо сначала «пропустить» через функцию CharToOem(), а уже потом отправлять на консольный вывод. Аналогично, если в программе есть консольный ввод текста и этот текст в дальнейшем надо сохранять в документах (файлах) с кодировкой ANSI, то перед сохранением его надо «пропустить» через функцию OemToChar().С учетом сказанного выше можно предложить следующую адаптацию нашей первой программы:

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "windows.h"

using namespace std;

int tmain(int argc, _TCHAR* argv[])

{

   char buff[256];

 CharToOemA("Добро пожаловать в С++", buff);

cout<< buff;

return 0;

}

2. Второй метод – с помощью функции setlocale( LC_ALL, "Russian" ):

 

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "windows.h"

using namespace std;

int tmain(int argc, _TCHAR* argv[])

{

   setlocale( LC_ALL, "Russian" );

 cout<<"Добро пожаловать в С++"<< endl;

return 0;

}

3. И, наконец, третий способ – с помощью подключения библиотеки <locate>:

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "windows.h"

#include <locate>

using namespace std;

int tmain(int argc, _TCHAR* argv[])

{

   LOCATE LOC("Russian Russia" );

 cout<<"Добро пожаловать в С++"<< endl;

return 0;

}

 

Конфигурация проекта. Visual C++ позволяет строить проект либо в отладочной конфигурации (Win32 Debug), либо в выпускной конфигурации (Win32 Release). Мы рекомендуем вам всегда работать с проектами в отладочной конфигурации. Обычно она установлена по умолчанию. Все же не мешает проверить, с какой конфигурацией на самом деле идет работа. Для этого выберите в меню Project пункт Settings... Откроется диалоговое окно Project Settings. Проверьте, какое значение установлено в окне комбинированного списка Settings For:.. Если это не Win32 Debug, то переключитесь на нужное значение через команду меню BuildSet Active Configuration...

Завершение работы над проектом. Можно выбрать меню File, пункт Close Workspace. А можно просто закрыть приложение Visual C++.

Открытие проекта, над которым вы работали ранее.

1. Способ первый:

  •  Запустите на выполнение Visual C++.
  •  Выберите меню File, пункт Open Workspace...
  •  В открывшемся диалоговом окне найдите папку с вашим проектом, а в ней найдите файл ProjectName.dsw.
  •  Откройте этот файл, щелкнув по нему мышью.

2. Способ второй:

  •  Запустите на выполнение Visual C++.
  •  Выберите меню File, наведите курсор мыши на пункт Recent Workspaces.
  •  Если в появившемся меню со списком последних файлов, с которыми шла работа, вы найдете интересующий вас файл ProjectName.dsw, то щелкните по нему мышью.

3.  Способ третий:

  •  Не вызывая Visual C++, найдите папку с вашим проектом, а в ней - файл ProjectName.dsw.
  •  Щелкните мышью на файле ProjectName.dsw.

Встроенная справочная система. В IDE Visual С ++ имеется обширная справочная система, доступная через меню Help главного окна.

Кроме этого, очень удобно пользоваться интерактивной справкой: если вы находитесь в окне Editor, поставьте текстовый курсор на интересующий вас оператор или библиотечную функцию C++ и нажмите клавишу F1. Тотчас будет вызвана справочная система MSDN с предоставлением необходимой информации. Если запрошенный термин встречается в разных разделах MSDN, то сначала появится диалоговое окно «Найденные разделы». В списке разделов выберите тот, в котором упоминается «Visual C++».

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

Установка точки прерывания. Точка прерывания позволяет остановить выполнение программы перед любой выполняемой инструкцией (оператором) с тем, чтобы продолжать выполнение программы либо в пошаговом режиме, либо в непрерывном режиме до следующей точки прерывания.

Чтобы задать точку прерывания перед некоторым оператором, необходимо установить перед ним текстовый курсор и нажать клавишу F9 или щелкнуть мышью на кнопке Insert/Remove Breakpoint на панели инструментов Build MiniBar. Точка прерывания обозначается в виде коричневого кружка на левом поле окна редактирования. Повторный щелчок на указанной кнопке снимает точку прерывания. В программе может быть несколько точек прерывания.

Выполнение программы до точки прерывания. Программа запускается в отладочном режиме с помощью команды BuildStart DebugGo (или нажатием клавиши F5).

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

Рисунок 1.6 - Запуск отладчика в Microsoft Visual C++

Среди различных команд этого меню особый интерес представляют команды Step Into (F11), Step Over (F10), Step Out (Shift+F11), Run To Cursor (Ctrl+F10) и Stop Debugging (Shift+F5).

Выбор последней команды (или нажатие комбинации клавиш Shift+F5) вызывает завершение работы с отладчиком.

 Пошаговое выполнение программы. Нажимая клавишу F10, можно выполнять один оператор программы за другим.

Предположим, что при пошаговом выполнении программы вы дошли до строки, в которой вызывается некоторая функция func(). Если вы хотите пройти через код вызываемой функции, то надо нажать клавишу F11. Если же внутренняя работа функции вас не интересует, а интересен только результат ее выполнения, то надо нажать клавишу F10.

Допустим, что вы вошли в код функции func(), нажав клавишу F11, но через несколько строк решили выйти из него, т.е. продолжить отладку после возврата из функции. В этом случае надо нажать клавиши Shift+F11.

Существует и другая возможность пропустить пошаговое выполнение некоторого куска программы: установите текстовый курсор в нужное место программы и нажмите клавиши Ctrl+F10.

Создание первой программы. Необходимо ввести в редакторе кода текст, представленный в листинге 1.1.

Листинг 1.1

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

cout << “Hello, world!” << endl;

cout << “Press any key to stop the program…”;

return 0;

}

Рисунок 1.7 - Текст программы в Microsoft Visual C++

Это и есть наша первая программа на языке C++ в среде Visual С++. По традиции она выводит на экран надпись «Hello, World!» (Здравствуй, Мир!). Далее программа поясняет, что для ее завершения необходимо нажать какую-либо клавишу на клавиатуре.      

Запустите программу на выполнение. Если вы все сделали правильно, на экране появится окно с соответствующими надписями. Чтобы закрыть это окно, нажмите любую клавишу на клавиатуре (рис. 1.8).

Рисунок 1.8 - Результаты работы программы

После того как программа завершилась, вы можете запустить ее еще раз тем же способом. Если между запусками в текст программы не были внесены изменения, то, запуская программу повторно, вы обнаружите, что запуск выполняется быстрее. Это происходит потому, что при первом запуске Visual C++ должен скомпилировать программу, то есть преобразовать ее исходный текст в исполнимый машинный код.

Основные сведения о программах на языке C++. Состав языка С++:

Алфавит языка или его символы - это основные неделимые знаки, с помощью которых пишутся все тексты на языке.

Лексема или элементарная конструкция - это минимальная единица языка, имеющая самостоятельный смысл.

Выражение задает правило вычисления некоторого значения.

Оператор задает законченное описание некоторого действия.

Алфавит C++ включает:

  •  прописные и строчные латинские буквы и знак подчеркивания;
  •  арабские цифры от 0 до 9;
  •  специальные знаки: “ { } , | [ ] ( ) + - / % * . \ ‘ : ? < = > ! & # ~ ; ^
  •  пробельные символы: пробел, символы табуляции, символы перехода на новую строку.

Из символов алфавита формируются лексемы языка:

  •  идентификаторы;
  •  ключевые (зарезервированные) слова;
  •  знаки операций;
  •  константы;
  •  разделители (скобки, точка, запятая, пробельные символы).

Основная программная единица на языке C++ - это текстовый файл с названием имя.cpp, где cpp - это принятое расширение для программ на C++, а имя - определяется исходя из семантики разрабатываемой программы.

Текстовый файл с программой на C++ вначале обрабатывает препроцессор, который распознает команды (директивы) препроцессора (каждая такая команда начинается с символа «#») и выполняет их. Практически в каждой программе на C++ используется процессорная команда

#include <имя_включаемого_(заголовочного)_файла>

 

Стандартные библиотечные функции. Все стандартные функции имеют прототип в соответствующем заголовочном файле. Далее в таблице  1.1 приведен список наиболее часто используемых заголовочных файлов, присутствующий почти в каждом компиляторе C++.

Таблица 1.1 - Типы заголовочных  файлов

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

Назначение

assert.h

Содержит декларации функций для диагностики программ

ctype.h

Содержит декларации функций преобразования типов

errno.h

Содержит описание кодов ошибок

floaf.h

Содержит декларации математических функций для работы с вещественными числами одинарной точности

limits.h

Содержит определение границ диапазона изменения значений переменных различных типов

locale.h

Содержит функции для поддержки национальных стандартов

math.h

Содержит декларации математический функций для работы с вещественными числами двойной точности

sefjmp.h

Содержит декларации функций для осуществления межсегментных переходов (используется только в операционной системе MS-DOS)

stdarg.h

Содержит макросы для работы с функциями с переменным числом аргументов

stdio.h

Содержит декларации функций ввода-вывода в C++

stdlib.h

Содержит декларации функций библиотеки времени исполнения (RTL - Runtime Library) C++

string.h

Содержит декларации функций для работы с ASCIIZ строками

conio.h

Содержит декларации функций консольного ввода-вывода (используется только в операционной системе MS-DOS)

time.h

Содержит декларации функций для работы с датой/временем

dos.h

Содержит декларации функций, используемые для обращения к сервисам DOS (используется только в операционной системе MS-DOS)

iostream.h

Содержит объекты и классы потокового ввода-вывода

Программа на языке C++ состоит из функций, описаний и директив препроцессора. Одна из функций должна иметь имя main() (либо WinMain, если программа пишется для операционной системы Windows).

Простейшее определение функции имеет следующий формат:

тип возвращаемого значения имя ([ параметры ])

{

операторы, составляющие тело функции

}

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

Сведения:

  •  если функция не должна возвращать значение, указывается тип void;
  •  тело функции является блоком и, следовательно, заключается в фигурные скобки;
  •  функции не могут быть вложенными;
  •  каждый оператор заканчивается точкой с запятой (кроме составного оператора).

Каждая программа должна содержать функцию с именем main. Эта функция является точкой входа, с которой начинается выполнение программы. Обычно в main() отсутствуют параметры. Тело функции всегда заключается в фигурные скобки. Ключевое слово int используется для обозначения того факта, что функция возвращает некоторое целочисленное значение, которое затем будет использовано операционной системой для определения факта завершения программы с ошибкой. Если ошибок не было, то функция main должна возвратить значение, равное 0.

Листинг 1.2

// Приложение с использованием стандартной

// библиотеки ввода-вывода

#include “stdafx.h”

#include <stdio.h>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

printf(“\tHello world!\n”);

return 0;

}

Листинг 1.3

// Приложение с использованием потоковой

// библиотеки ввода-вывода

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

cout << ‘\t’ << “Hello world” << ‘\n’;

// можно и так: cout << “\tHello world!\n”;

return 0;

}

Эта программа выводит на экран фразу «Hello world!». Кроме  переменных, cout и printf воспринимают еще и управляющие символы, такие как \n - символ перевода строки. Все, что встречается после символов \n, будет показано с начала следующей строки. В приведенном примере кроме \n присутствует еще и символ табуляции \t. Он поводит к тому, что фраза «Hello world!» окажется не в начале строки, а с некоторым отступом вправо.

Последовательности символов, начинающиеся с обратной косой черты, называют управляющими, или escape-последовательностями. Управляющая последовательность интерпретируется как одиночный символ. Если непосредственно за обратной косой чертой следует символ, не предусмотренный таблице, результат интерпретации не определен. Если в последовательности цифр встречается недопустимая, она считается концом цифрового кода.

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

"Открытое Акционерное Общество \"Волна\""

Все строковые литералы рассматриваются компилятором как различные объекты. Строковые константы, отделенные в программе только пробельными символами, при компиляции объединяются в одну. Длинную строковую константу можно разместить на нескольких строках, используя в качестве знака переноса обратную косую черту, за которой следует перевод строки. Эти символы игнорируются компилятором, при этом следующая строка воспринимается как продолжение предыдущей.

Управляющие символы (табл. 1.2) заключаются в одинарные кавычки и начинаются с обратной косой черты (обратного слеша). Не будь ее, объект cout и функция printf воспринял бы запись 'n' как строчную латинскую букву «n», а 't' - как латинскую букву «t» и т.д. Поэтому фразу «Hello world!» можно вывести на экран «по буквам».

Таблица 1.2 - Управляющие символы 

Название

Обозначение

Символ новой строки

\n

Горизонтальная табуляция

\t

Вертикальная табуляция

\v

Возврат на шаг

\b

Возврат каретки

\r

Обратная слэш

\\

Апостроф

\’

Двойная кавычка

\

Нулевой символ

\0

Звуковой сигнал

\a

Перевод страницы (формата)

\f

Восьмеричный код символа

\0ddd

Шестнадцатеричный код символа

\0xddd

Листинг 1.4

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

cout << ‘\t’ << ‘H’ << ‘e’ << ‘l’ << ‘l’ << ‘o’

 << ‘\x20’ << ‘w’ << ‘o’ << ‘r’ << ‘l’ << ‘d’

 << ‘!’ << ‘\n’;

 return 0

}

Одна из особенностей языка C++ - так называемая перегрузка дает необязательным использование указателей формата. В отличие от функции printf(), которая требует обязательное указание формата, cout при передаче параметров сам определяет формат на основании типа получаемых данных. Этот процесс называется перегрузкой. Аналогично оператору потокового вывода cout работает оператор потокового ввода cin:

cin >> t;

где t - переменная, значение которой необходимо ввести пользователем.

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

В программах на C++, использующих консоль, должна присутствовать функция main(), в отличие от приложений Windows, где точкой входа является функция WinMain(). Круглые скобки являются частью имени функции и ставить их надо обязательно, так как именно они указывают компилятору, что имеется в виду функция, а не просто английское слово main. В противном случае компиляция не будет завершена. Фактически каждая функция включает в свое имя круглые скобки, но в большинстве случаев в них содержится некая информация. В дальнейшем в тексте книги, ссылаясь на функцию, мы всегда будем ставить после ее имени круглые скобки.

Следом за main() вводятся инструкции. Инструкции могут быть представлены в виде стандартных команд и имен функций, содержащихся в библиотеках или написанных вами самостоятельно. Прямая, или открывающая фигурная скобка ({) помещается перед первой инструкцией, а обратная, или закрывающая фигурная скобка (}) следует за последней инструкцией.

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

При запуске программы компьютер начинает ее выполнение с первой инструкции функции main().

Функции консольного ввода-вывода clrscr(), getch(), cputs(). Поскольку разрабатываемые в данном методическом указании программы будут совместимы с MS-DOS, то рассмотрим функции консольного ввода-вывода.

Все прототипы этих функций находятся в файле conio.h. Приведем краткое назначение каждой из этих функций.

Функция clrscr() предназначена для очистки экрана.

Функция getch() предназначена для ввода одного символа с клавиатуры. Она ожидает нажатие клавиши и не отображает введенный символ на экран. Часто эту функцию используют для задержки окна вывода.

Функция puts() выводит на экран текстовую константу, заключенную в кавычки, в конце строки в обязательном порядке печатаются символы ‘\r и ‘\n, иными словами при использовании данной функции курсор автоматически переводится на новую строку. В отличие от предыдущих двух функций, данная функция входит в библиотеку стандартного ввода-вывода, и для ее использования необходимо подключить заголовочный файл stdio.h. Аналогом данной функции в библиотеке консольного ввода-вывода служит функция cputs().

Ниже приведен листинг программа на C++, которая выводит на экран монитора фразу «Hello world!» с помощью описанных функций.

Листинг 1.5

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

puts(“Hello world!”);

cputs(“Hello world!”);

}

Кавычки, отмечающие слово внутри круглых скобок в функции puts() не выводятся на экран. В языке C++ они означают, что на экран следует вывести заключенную в них последовательность символов, а не константу или переменную.

Точка с запятой в языке C/C++ является разделителем и отмечает конец инструкции. Разделитель показывает компилятору что данная инструкция завершена и дальше начинается следующая инструкция или заканчивается программа. Точку с запятой необходимо ставить  после каждой отдельной инструкции, так, как это показано в листинге 1.5. Вышесказанное не означает, что точку с запятой надо ставить в конце каждой строки. Иногда инструкция занимает больше одной строки и в этом случае точку с запятой надо ставить только один раз, отмечая конец команды.

C++ является языком свободного формата. Это означает, что для него не имеет значения, где будут помещены ограничители и начало строки. С таким же успехом можно написать программу следующим образом:

int main(){puts("Hello world!"); return 0}

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

  •  помещать функцию main() на отдельной строке;
  •  помещать фигурные скобки на отдельных строках;
  •  создавать в тексте программы отступы с помощью табуляции.

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

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

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

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

Тому, кто только начинает программировать, введение комментариев не нужной  роскошью, ведь он и так тратит массу времени на создание программы. Действительно, небольшой, простой, легко тестируемой программе, ее из небольшого числа строк, комментарии не нужны.  Однако, читая программу, вы будете благодарны ее автору за любые пояснения, пусть программа не так уж и сложна.

Комментарий либо начинается с двух символов «прямая косая черта» (//) и заканчивается символом перехода на новую строку, либо заключается между символами-скобками /* и */. Внутри комментария можно использовать любые допустимые на данном компьютере символы, а не только символы из алфавита языка C++, поскольку компилятор комментарии игнорирует. Вложенные комментарии-скобки стандартом не допускаются, хотя в некоторых компиляторах разрешены.

Листинг 1.6

/*Эта программа выводит сообщение на экран*/

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

 puts("Hello world!");

 return 0;

}

Символы /* указывают начало строки комментария, а символы */ отмечают ее конец. Все, что помещено между ними, C++ игнорирует. Как правило, в начале текста программы программисты помещают строку комментария чтобы пояснить цель ее создания. Внутри текста программы помещаются комментарии, поясняющие употребление отдельных инструкций или последовательность логических шагов. Такие комментарии обычно помещают после разделителя, то есть точки с запятой:

Листинг 1.7

/*Эта программа выводит сообщение на экран */

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

/*На экран выводится сообщение "ОК"*/

 puts("Hello world");

 return 0; /*Возврат в операционную систему*/

}

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

Весьма распространенным недосмотром, который часто допускают начинающие программисты, является то, что они забывают ставить символы */ в конце комментария, и в результате получают сообщение об ошибке компилятора, в C++ использование комментария несколько облегчается за счет введения пары символов //, указывающих начало строки комментария. В этом с концом комментария считается конец строки, так что нет необходимости отмечать его специальным символом:

Листинг 1.8

//Эта программа выводит сообщение на экран

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

 puts("Hello world!");

return 0; //Возврат в операционную систему

}

Однако если  комментарий занимает больше одной строки, каждая строка должна начинаться с символов //.

Литералы. Идентификаторы, константы, переменные, ключевые слова. Литералом называется любой элемент данных, который вводится непосредственно в инструкции языка C++. Литералом может являться любое число, символ или строка, которые вводятся как начальные значения переменной. Например: cout=8, где число 8 являться литералом.

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

Идентификатор - это имя программного объекта. В идентификаторе могут использоваться латинские буквы, цифры и знак подчеркивания. Прописные и строчные буквы различаются, например, sysop, SySoP и SYSOP - три различных имени. Первым символом идентификатора может быть буква или знак подчеркивания, но не цифра. Пробелы внутри имен не допускаются.

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

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

Константами называют неизменяемые величины. Различаются целые, вещественные, символьные и строковые константы. Компилятор, выделив константу в качестве лексемы, относит ее к одному из типов по ее внешнему виду. Форматы констант, соответствующие каждому типу, приведены в таблице 1.3.

Таблица 1.3 - Форматы констант, соответствующие каждому типу

Константа

Формат

Примеры

Целая

Десятичный: последовательность десятичных цифр, начинающаяся не с нуля, если это не число нуль

8, 0, 199226

Восьмеричный: нуль, за которым следуют восьмеричные цифры (0,1,2,3,4,5,6,7)

01, 020, 07155

Шестнадцатеричный: 0х или 0Х, за которым следуют шестнадцатеричные цифры (0,l,2,3,4,5,6,7,8,9,A,B,C,D,E,F)

0хА, 0x1B8, 0X00FF

Вещественная

Десятичный: [цифры].[цифры]

5.7, .001, 35.

Экспоненциальный:

[цифры][.][цифры]{Е|е}[+-][ЦиФры]

0.2Е6, .lle-3, 5E10

Символьная

Один или два символа, заключенных в апострофы

‘А’, ‘ю’, ‘*’, ‘db’, ‘\0’, ‘\n’, ‘\012’, ‘\x07\x07’

Строковая

Последовательность символов, заключенная в кавычки

“Здесь был Vasia”, “\tЗначение r=\0xF5\n”

Если требуется сформировать отрицательную целую или вещественную константу, то перед константой ставится знак унарной операции изменения знака (-), например: -218, -022, -0хЗС, -4.8, -0.1е4.

Вещественная константа в экспоненциальном формате представляется в виде мантиссы и порядка. Мантисса записывается слева от знака экспоненты (Е или е), порядок - справа от знака. Значение константы определяется как произведение мантиссы и возведенного в указанную в порядке степень числа 10. Обратите внимание, что пробелы внутри числа не допускаются, а для отделения целой части от дробной используется не запятая, а точка.

Символьные константы, состоящие из одного символа, занимают в памяти один байт и имеют стандартный тип char. Двухсимвольные константы занимают два байта и имеют тип int, при этом первый символ размещается в байте с меньшим адресом.

Символ обратной косой черты используется для представления:

  •  кодов, не имеющих графического изображения (например, \a - звуковой сигнал, \n - перевод курсора в начало следующей строки);
  •  символов апострофа ( ' ), обратной косой черты ( \ ), знака вопроса (?) и кавычки ( " );
  •  любого символа с помощью его шестнадцатеричного или восьмеричного кода, например, \073, \0xF5. Числовое значение должно находиться в диапазоне от 0 до 255.

Определить константу - это, значит, сообщить компилятору C ее имя, тип и значение. Для определения константы в языке C перед функцией main() часто помещают директиву #define для объявления констант и макроопределений. Однако использование этой директивы сопряжено с появлением трудно диагностируемых ошибок, лучшей альтернативой для констант является использование переменных с модификатором const. Такие переменные носят название константных переменных и объявляются следующий образом:

const <Тип> <Имя_Переменной> = <Значение>;

Наличие инициализатора <Значение> при объявлении константной переменной обязательно. Значение константной переменной не может быть изменено в процессе работы программы. Например:

const double pi = 3.1415926; // вещественная кон.

const int size = 400; // целая константа

const char new_line = ‘\n’; // символьная константа

const char topic[] = “Тема”; // строковая константа

В любой программе требуется производить вычисления. Для вычисления значений используются выражения, которые состоят из операндов, знаков операций и скобок. Операнды задают данные для вычислений. Операции задают действия, которые необходимо выполнить. Каждый операнд является, в свою очередь, выражением или одним из его частных случаев, например, константой или переменной. Операции выполняются в соответствии с приоритетами. Для изменения порядка выполнения операций используются круглые скобки. Рассмотрим составные части выражений и правила их вычисления.

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

int a; double x;

Общий вид оператора описания переменных:

 

[класс памяти] [const|volatile] тип имя[инициализатор];

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

  •  большие и малые буквы латинского алфавита,
  •  арабские цифры,
  •  символ подчеркивания «_».

Компилятор C++ рассматривает одну и ту же большую и малую буквы как разные символы. Например, переменные dx_1, Dx_1, dX_1, DX_1 имеют различные имена и являются, соответственно, разными литералами.

Первым символом в имени должна стоять буква или знак подчеркивания. Например: Num, _min, max_, sum_7, и так далее. Тем не менее, не рекомендуется использовать знак подчеркивания в качестве первого символа литерала, поскольку таковые в основном используются для системных нужд и определяют литералы собственно компилятора или операционной системы, например _UNICODE, литерал, используемый  в директивах условной компиляции, если программа написана с использованием кодировки Unicode.

Определить переменную - это, значит, сообщить ее имя и тип компилятору C++. Объявление переменной можно совместить  с ее инициализацией. В этом  случае объявлений переменной записывают следующим способом:

<Тип> <Имя_Переменной> = <Начальное_значение>;

Знак «=» обозначает инструкцию присваивания. Она предназначена для изменения значения переменных

<имя>=<выражение>;

Разновидность операции присваивания

<имя>=<имя> <знак операции> <выражение>;

Имя переменной не должно совпадать с ключевыми словами языка C++, перечень которых, приведен в табл. 1.4.

Ключевые слова - это зарезервированные идентификаторы, которые имеют специальное значение для компилятора. Их можно использовать только в том смысле, в котором они определены.

Таблица 1.4 - Ключевые слова стандарта языка C++

asm

else

new

this

auto

enum

operator

throw

bool

explicit

private

true

break

export

protected

try

case

extern

public

typedef

catch

false

register

typeid

char

float

reinterpret_cast

typename

class

for

return

union

const

friend

short

unsigned

const_cast

goto

signed

using

continue

if

sizeof

virtual

default

int

static

void

delete

inline

static_cast

volatile

do

long

struct

wchar_t

double

mutable

switch

while

dynamic_cast

namespace

template

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

Время жизни может быть постоянным (в течение выполнения программы) и временным (в течение выполнения блока).

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

Для задания класса памяти используются следующие спецификаторы:

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

extern - означает, что переменная определяется в другом месте программы (в другом файле или дальше по тексту). Используется для создания переменных, доступных во всех модулях программы, в которых они объявлены.

static - статическая переменная. Время жизни - постоянное. Инициализируется один раз при первом выполнении оператора, содержащего определение переменной. В зависимости от расположения оператора описания статические переменные могут быть глобальными и локальными. Глобальные статические переменные видны только в том модуле, в котором они описаны.

register - аналогично auto, но память выделяется по возможности в регистрах процессора. Если такой возможности у компилятора нет, переменные обрабатываются как auto.

int a;                   // 1 глобальная переменная а

int main()

{

int b;              // 2 локальная переменная b

extern int x;       /* 3 переменная х определена в                    другом месте */

static int с;       /* 4 локальная статическая переменная с */

а = 1;      // 5 присваивание глобальной переменной

int a;      // 6 локальная переменная а

а = 2;      // 7 присваивание локальной переменной

::а = 3;    // 8 присваивание глобальной переменной

return 0;

}

int x = 4;      // 9 определение и инициализация х

В этом примере глобальная переменная a определена вне всех блоков. Память под нее выделяется в сегменте данных в начале работы программы, областью действия является вся программа. Область видимости - вся программа, кроме строк 6-8, так как в первой из них определяется локальная переменная с тем же именем, область действия которой начинается с точки ее описания и заканчивается при выходе из блока. Переменные b и с - локальные, область их видимости - блок, но время жизни различно: память под b выделяется в стеке при входе в блок и освобождается при выходе из него, а переменная с располагается в сегменте данных и существует все время, пока работает программа.

Если при определении начальное значение переменных явным образом не задается, компилятор присваивает глобальным и статическим переменным нулевое значение соответствующего типа. Автоматические переменные не инициализируются.

Имя переменной должно быть уникальным в своей области действия (например, в одном блоке не может быть двух переменных с одинаковыми именами).

Описание переменной может выполняться в форме объявления или определения. Объявление информирует компилятор о типе переменной и классе памяти, а определение содержит, кроме этого, указание компилятору выделить память в соответствии с типом переменной. В C++ большинство объявлений являются одновременно и определениями. В приведенном выше примере только описание 3 является объявлением, но не определением.

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

Простые типы данных. Прежде чем использовать в программе на языке C++ какую-либо переменную, ее необходимо описать. Переменные в языке C++ могут быть описаны как в теле функции (между { }), так и вне ее. При описании переменной внутри функции область ее действия ограничена функцией. При описании переменной указать тип переменной и ее имя (идентификатор), для того, чтобы C++ должен зарезервировать достаточное количество памяти для хранения введенной информации. Разные типы данных занимают не одинаковый объем памяти. Не все функции языка C могут работать с данными любого типа.

В процессе написания программы необходимо определить все данные, которые будут использоваться, причем сделать это надо и для вводимой информации, и для результата. Данные классифицируются по типу значений, которые они содержат. Значение не обязательно означает числовую величину, но и буквы, слова, фразы.

Тип данных определяет:

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

Рассмотрим наиболее часто употребляемые простые типы языка C++:

  •  целые числа со знаком int, long, short,
  •  целые беззнаковые константы unsigned,
  •  символы, занимающие один байт char и два байта wchar_t,
  •  числа с плавающей точкой float, double.

Тип char определяет целочисленные переменные, занимающие один байт, в диапазоне от -128 до 127. Этот тип, как правило, применяется для символьных переменных (числовым значением является код символа).

Тип wchar_t определяет целочисленные переменные, занимающие два байта, в диапазоне от -32768 до 32767. Этот тип, как правило, применяется для символьных переменных в кодировке Unicode (числовым значением является код символа в кодировке Unicode).

Тип short определяет целые переменные, занимающие два байта, в диапазоне от -32768 до 32767. Этот тип используется для небольших целых чисел, в основном для управления циклами.

Тип long определяет целочисленные переменные, занимающие четыре байта, в диапазоне от -2147483647 до 2147483646.

В зависимости от компилятора и операционной системы тип int может быть эквивалентен либо типу short, либо типу long. Для компилятора Borland C++ Builder тип int эквивалентен типу long.

Беззнаковые типы unsigned определяяют беззнаковые целые числа. Это ключевое слово используется с другими типами данных для определения этого типа как беззнакового, т.е. только положительные числа и ноль. К беззнаковым типам относятся unsigned char, unsigned wchar_t, unsigned short, unsigned long, unsigned int. Тип unsigned эквивалентен типу unsigned int.

Тип float определяет переменные, занимающие четыре байта, для чисел с плавающей точкой в диапазоне от  до  

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

Таблица 1.5 - Диапазоны значений простых типов данных на IBM PC

Тип

Диапазон значений

Размер (байт)

bool

true и false

1

signed char

-128 ... 127

1

unsigned char

0 ... 255

1

signed short int

-32 768 ... 32 767

2

unsigned short int

0 ... 65 535

2

signed long int

-2 147 483 648 ... 2 147 483 647

4

unsigned long int

0 ... 4 294 967 295

4

float

3.4e-38 ... 3.4c+38

4

double

1.7e-308 ... 1.7e+308

8

long double

3.4c-4932 ... 3.4e+4932

10

При описании данных достаточно ввести тип, за которым должен следовать список имен переменных. Например:

int tdw, _sek, g1o; char elen, ogi;

long kid, g2o; char isi ='j';

float z2_c; unsigned rib = 6;

double pi = 3.14159;

Операторы языка С++. Рассмотрим список всех операций, определенных в языке C++, в соответствии с их приоритетами (по убыванию приоритетов, операции с разными приоритетами разделены чертой). В соответствии с количеством операндов, которые используются в операциях, они делятся на унарные (один операнд), бинарные (два операнда) и тернарную (три операнда).

Все приведенные в таблице 1.6 операции, кроме условной «? :», sizeof, селектора членов класса «.», доступа к области видимости «::» и оператора последовательного вычисления «,» могут быть перегружены.

Таблица 1.6 - Операции, определенные в С++

Операция

Краткое описание

Унарные операции

::

Доступ к области видимости

.

Селектор членов класса

->

Селектор членов класса

[ ]

Индексация

( )

Вызов функции

++

Постфиксный инкремент

--

Постфиксный декремент

typeid

Идентификация типа

dynamic_cast

Преобразование типа с проверкой на этапе выполнения

static_cast

Преобразование типа с проверкой на этапе компиляции

reinterpret_cast

Преобразование типа без проверки

const_cast

Константное преобразование типа

sizeof

Размер объекта или типа

--

Префиксный декремент

++

Префиксный инкремент

~

Поразрядное отрицание

!

Логическое отрицание

-

Арифметическое отрицание (унарный минус)

+

Унарный плюс

&

Взятие адреса

*

Разадресация

new

Выделение памяти

delete

Освобождение памяти

(<тип>)

Преобразование типа

.*

Селектор членов класса по указателю

->*

Селектор членов класса по указателю

Бинарные и тернарные операции

*

Умножение

/

Деление

%

Остаток от деления

+

Сложение

-

Вычитание

<<

Сдвиг влево

>>

Сдвиг вправо

<

Меньше

<=

Меньше или равно

>

Больше

>=

Больше или равно

==

Равно

!=

Не равно

&

Поразрядная конъюнкция (И)

^

Поразрядное исключающее ИЛИ

|

Поразрядная дизъюнкция (ИЛИ)

&&

Логическое И

| |

Логическое ИЛИ

? :

Условная операция (тернарная)

=

Присваивание

*=

Присваивание с умножением

/=

Деление с присваиванием

%=

Остаток от деления с присваиванием

+=

Сложение с присваиванием

-=

Вычитание с присваиванием

<<=

Сдвиг влево с присваиванием

>>=

Сдвиг вправо с присваиванием

&=

Поразрядное И с присваиванием

|=

Поразрядное ИЛИ с присваиванием

^=

Исключающее ИЛИ с присваиванием

throw

Инициировать исключение

,

Последовательное вычисление

Рассмотрим основные операции подробнее.

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

В отличие от других языков программирования в Си и C++ инструкция присваивания, выполняющая некоторое действие, может быть записана несколькими способами.

Например, вместо x = x + dx  можно записать x += dx, вместо у = у*х  можно записать у *= х а вместо i = i + 1 воспользоваться оператором инкремента и записать i++. Список наиболее часто используемых таких операций приведен в таблице 1.7.

Таблица 1.7 - Арифметические операции с присваиванием в C++

Наименование операции

Выражение

Эквивалентное

выражение

Префиксный и постфиксный инкремент

++x или x++

x = x + 1

Префиксный и постфиксный декремент

--x или x--

x = x - 1

Сложение с присваиванием

x += y

x = x + y

Вычитание с присваиванием

x -= y

x = x - y

Умножение с присваиванием

x *= y

x = x * y

Деление с присваиванием

x /= y

x = x / y

Взятие остатка с присваиванием

x %= y

x = x % y

Операндом операции инкремента в общем случае является так называемое L-значение (L-value). Так обозначается любое выражение, адресующее некоторый участок памяти, в который можно занести значение. Название произошло от операции присваивания, поскольку именно ее левая (Left) часть определяет, в какую область памяти будет занесен результат операции. Переменная является частным случаем L-зиачепия.

Операция определения размера sizeof предназначена для вычисления размера объекта или типа в байтах, и имеет две формы:

sizeof выражение

sizeof (тип)

Пример:

#include <iostream.h>

int main()

{

float x = 1;

cout  << "sizeof (float) :" << sizeof (float);

cout << "\nsizeof x :" << sizeof x;

cout  << "\nsizeof (x + 1.0) :" << sizeof(x + 1.0);

return 0:

}

Результат работы программы:

sizeof (float) : 4

sizeof x : 4

sizeof (x + 1.0) : 8

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

Операции отрицания (-, ! и ~). Арифметическое отрицание (унарный минус -) изменяет знак операнда целого или вещественного типа па противоположный. Логическое отрицание (!) дает в результате значение 0, если операнд есть истина (не нуль), и значение 1, если операнд равен нулю. Операнд должен быть целого или вещественного типа, а может иметь также тип указатель. Поразрядное отрицание (~), часто называемое побитовым, инвертирует каждый разряд в двоичном представлении целочисленного операнда.

Деление (/) и остаток от деления (%). Операция деления применима к операндам арифметического типа. Если оба операнда целочисленные, результат операции округляется до целого числа, в противном случае тип результата определяется правилами преобразования. Операция остатка от деления применяется только к целочисленным операндам. Знак результата зависит от реализации.

Операции сдвига (<< и >>) применяются к целочисленным операндам. Они сдвигают двоичное представление первого операнда влево или вправо на количество двоичных разрядов, заданное вторым операндом. При сдвиге влево (<<) освободившиеся разряды обнуляются. При сдвиге вправо (>>) освободившиеся биты заполняются нулями, если первый операнд беззнакового типа, и знаковым разрядом в противном случае. Операции сдвига не учитывают переполнение и потерю значимости.

Операции отношения (<, <=. >, >=, ==. !=) сравнивают первый операнд со вторым. Операнды могут быть арифметического типа или указателями. Результатом операции является значение true или false (любое значение, не равное нулю, интерпретируется как true). Операции сравнения на равенство и неравенство имеют меньший приоритет, чем остальные операции сравнения.

Поразрядные операции (&, |, ^) применяются только к целочисленным операндам и работают с их двоичными представлениями. При выполнении операций операнды сопоставляются побитово (первый бит первого операнда с первым битом второго, второй бит первого операнда со вторым битом второго, и т.д.). При поразрядной конъюнкции, или поразрядном И (операция обозначается &) бит результата равен 1 только тогда, когда соответствующие биты обоих операндов равны 1.

При поразрядной дизъюнкции, или поразрядном ИЛИ (операция обозначается |) бит результата равен 1 тогда, когда соответствующий бит хотя бы одного из операндов равен 1.

При поразрядном исключающем ИЛИ (операция обозначается ^) бит результата равен 1 только тогда, когда соответствующий бит только одного из операндов равен 1.

Логические операции (&& и | |). Операнды логических операций И (&&) и ИЛИ (| |) могут иметь арифметический тип или быть указателями, при этом операнды в каждой операции могут быть различных типов. Преобразования типов не производятся, каждый операнд оценивается с точки зрения его эквивалентности нулю операнд, равный нулю, рассматривается как false, не равный нулю - как true). Результатом логической операции является true или false. Результат операции логическое И имеет значение true только если оба операнда имеют значение true. Результат операции логическое ИЛИ имеет значение true, если хотя бы один из операндов имеет значение true. Логические операции выполняются слева направо. Если значения первого операнда достаточно, чтобы определить результат операции, второй операнд не вычисляется.

Операции присваивания (=, +=, -=, *= и т.д.). Операции присваивания могут использоваться в программе как законченные операторы. Формат операции простого присваивания (=):

операнд_1 = операнд_2

Первый операнд должен быть L-значением, второй - выражением. Сначала вычисляется выражение, стоящее в правой части операции, а потом его результат записывается в область памяти, указанную в левой части (мнемоническое правило: «присваивание - это передача данных "налево"»). То, что ранее хранилось в этой области памяти, естественно, теряется.

В сложных операциях присваивания ( +=, *=, /= и т.п.) при вычислении выражения, стоящего в правой части, используется и L-значение из левой части. Например, при сложении с присваиванием ко второму операнду прибавляется первый, и результат записывается в первый операнд, то есть выражение, а +=b является более компактной записью выражения a = a + b.

Условная операция (? :). Эта операция тернарная, то есть имеет три операнда. Ее формат:

операнд_1 ? операнд_2 : операнд_3

Первый операнд может иметь арифметический тип или быть указателем. Он оценивается с точки зрения его эквивалентности нулю (операнд, равный нулю, рассматривается как false, не равный пулю - как true). Если результат вычисления первого операнда равен true, то результатом условной операции будет значение второго операнда, иначе - третьего операнда. Вычисляется всегда либо второй операнд, либо третий. Их тип может различаться. Условная операция является сокращенной формой условного оператора if.

#include <stdio.h>

int main()

{

int a = 11, b = 4, max;

max = (b > a)? b : a;

printf(“Наибольшее число: %d”, max);

return 0;

}

Результат работы программы:

Наибольшее число: 11.

Выражения языка С++. Выражения состоят из операндов, знаков операций и скобок и используются для вычисления некоторого значения определенного типа. Каждый операнд является, в свою очередь, выражением или одним из его частных случаев - константой или переменной. Примеры выражений:

(а + 0.12) / 6

 х && у | | !z

(t * sin(x) - 1.05e4)/((2 * k + 2) * (2 * k + 3))

Операции выполняются в соответствии с приоритетами. Для изменения порядка выполнения операций используются круглые скобки. Если в одном выражении записано несколько операций одинакового приоритета, унарные операции, условная операция и операции присваивания выполняются справа налево, остальные - слева направо. Например, а = b = с означает а = (b = с), а а + b + с означает (а + b) + с. Порядок вычисления подвыражений внутри выражений не определен: например, нельзя считать, что в выражении (sin(x + 2) + cos (у + 1)) обращение к синусу будет выполнено раньше, чем к косинусу, и что х + 2 будет вычислено раньше, чем y + 1.

Результат вычисления выражения характеризуется значением и типом. Например, если а и b - переменные целого типа и описаны так:

int а = 2, b = 5;

то выражение а + b имеет значение 7 и тип int, а выражение а = b имеет значение, равное помещенному в переменную а (в данному случае 5) и тип, совпадающий с типом этой переменной. Таким образом, в C++ допустимы выражения вида а = b = с: сначала вычисляется выражение b = с, а затем его результат становится правым операндом для операции присваивания переменной а.

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

Преобразования бывают двух типов:

  •  изменяющие внутреннее представление величин (с потерей точности или без потери точности);
  •  изменяющие только интерпретацию внутреннего представления.

К первому типу относится, например, преобразование целого числа в вещественное (без потери точности) и наоборот (возможно, с потерей точности), ко второму - преобразование знакового целого в беззнаковое.

В любом случае величины типов char, signed char, unsigned char, short int и unsigned short int преобразуются в тип int, если он может представить все значения, или в unsigned int в противном случае.

После этого операнды преобразуются к типу наиболее длинного из них, и он используется как тип результата. Программист может задать преобразования типа явным образом.

Стандартные математические функции. В языке C для математических вычислений используются  стандартные математические  функции, декларированные в заголовочном файле math.h (табл. 1.8 и табл. 1.9).

Таблица 1.8- Тригонометрические и гиперболические функции

Тригонометрические функции (угол задается в радианах)

Обратные тригонометрические функции (возвращают угол в радианах)

Гиперболические функции

sin(x) - синус

asin(x) - арксинус

sinh(x) - гиперболический синус

cos(x) - косинус

acos(x) - арккосинус

cosh(x) - гиперболический косинус

tan(x) - тангенс

atan(x) - арктангенс

tanh(x) - гиперболический тангенс

atan2(y, x) - угол в полярных координатах точки (x, y) в диапазоне .

Таблица 1.9 - Другие наиболее часто используемые
математические функции

Функция

Действие

hypot(x,y)

вычисляет гипотенузу прямоугольного треугольника с катетами x и y

exp(x)

экспоненциальная функция,

log(x)

натуральный логарифм, ln(x), x > 0

log10(x)

десятичный логарифм, lg(x), x > 0

pow(x, y)

вычисляет . Ошибка области, если x = 0 и y ≤ 0 или

x < 0, y - не целое

pow10(p)

Вычисляет . Результат вычисляется в виде double.
Все аргументы считаются допустимыми, p типа
int.

sqrt(x)

корень квадратный из x, x ≥ 0

ceil(x)

находит наименьшее целое типа double, не меньшее x

ceil(6.25) = 6.00, ceil(-6.25) = -6.00

floor(x)

находит наибольшее целое типа double не превышающее значение x. floor(6.25) = 6.00, floor(-6.25) = -7.00

fabs(x)

абсолютное значение (модуль) числа с плавающей точкой
fabs(-6.25) = 6.25

Пример:

#include "stdafx.h"

#include "math.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

double x, z, s;

const double a=0.5, b=1.08, c=2.1, m=0.7;

z=(sin(x)/sqrt(1+m*m*sin(x*x))-c*m*log10(m*x));

cout<< "z=";

cout<< z;

s=(exp(-a*x)*sqrt(x+1)+exp(-b*x)*sqrt(x+1.5));

cout<< "\ns=";

cout<< s;

getch();

return 0;

}

В этом примере вычисляется функция:

при заданных значениях: х = 1.7; a = 0.5; b =1.08; c = 2.1; m =0.7.

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

К ошибкам, выявляемым на этапе компиляции, относятся, прежде всего, синтаксические ошибки (нарушения правил языка C++ в исходном тексте программы). Такие ошибки обычно выявляются самой средой программирования. Встретив синтаксическую ошибку в программе, прервет компиляцию и выдаст сообщение об ошибке.

К ошибкам времени выполнения программы относятся такие, которые возникают не в результате нарушения синтаксиса C++, а в результате выполнения программой каких-либо недопустимых действий (например, попытка разделить число на нуль или записать данные на заполненный диск). Некоторые из этих ошибок отличаются тем, что возникают лишь при определенных условиях, например при заполнении диска или обращении в нуль значения какой-либо переменной. Как правило, при возникновении ошибки времени выполнения программа выдает специальное сообщение об ошибке (и часто ее выполнение на этом завершается), Логические ошибки в программе относятся к самому «труднонаходимому» типу ошибок, поскольку их зачастую невозможно выявить формальными методами. При наличии логических ошибок программа выполняется и компилируется без проблем, но делает не то, что ждет от нее программист. Обнаружить и устранить логические ошибки (а также ошибки времени выполнения) в программе можно с помощью тестирования и отладки.

Сообщение об ошибке содержит указание на имя файла и строку, в которой обнаружена ошибка, код ошибки и ее краткое описание. Более подробные сведения об ошибке можно получить в справочной системе. Для этого нужно щелкнуть на строке сообщения об ошибке один раз (при этом строка будет выделена) и нажать клавишу F1. Если щелкнуть на строке сообщения об ошибке дважды, в окне редактора исходных текстов будет выделена строка, в которой обнаружена ошибка. Visual C++ выводит сразу все сообщения об ошибках, обнаруженных в программе.

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

Кроме сообщений об ошибках выводит предупреждения (warnings). Предупреждения означают, что компилятор обнаружил в программе фрагмент, который может быть ошибочным. Таким образом, компилятор пытается предотвратить возможные ошибки времени выполнения, и даже логические ошибки. Структура предупреждающего сообщения аналогична структуре сообщения об ошибках. Справочная система также предоставляет информацию о предупреждениях компилятора. Само появление предупреждений еще не означает, что вы обязательно должны что-то изменить в тексте программы, но к ним необходимо относиться к ним внимательно.

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15². Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Номер индивидуального задания студента , где - номер студента в журнале преподавателя. Порядок выполнения работы:

  1.  Оформить программу, указав свою фамилию и инициалы, группу с использованием однострочного и многострочного комментариев.
  2.  Объявить и проинициализировать переменные основных типов (int, double, char).
  3.  По инструкции cin >> ввести одно из предложенных значений переменных с клавиатуры (int, double).
  4.  Оставшиеся значения определить как константы, используя константные переменные.
  5.  Вычислить значения выражение по заданным расчетным формулам и наборам исходных данных.
  6.  Инструкцией cout << вывести на экран значения исходных данных и результаты вычислений, сопровождая вывод именами выводимых величин на экран. При переводе на новую строку использовать управляющий символ ‘/n и манипулятор endl.
  7.  Изменить программу таким образом, чтобы значения всех переменных вводились с клавиатуры.

Индивидуальное задание №1. Вариант:

Задание

Задание

  1.  

 

при х = 1.426; у = - 1.220;

 z = 3.5.

  1.  

 

при х = 1.4; a = 0.5; b = 3.1.

  1.  

 

при х = 1.825; у = 18.225;

 z = - 3.289.

  1.  

 

при х = 0.3; a = 0.5; b = 2.9.

  1.  

 

при х = 0.335; у = 0.025.

  1.  

 

при х = 1.426; у = -0.823;

 z = 2.724.

  1.  

 

при a = - 0.5; b = 1.7; t = 0.44.

  1.  

 

при х = 0.9; m = 1.2; c = 2.4.

  1.  

 

при a = - 1.5; b = 15.5;

x = -2.9.

  1.  

 

при х = 5.4; у = 1.9; b = 3.5;

 m = 0.2.

  1.  

 

при a = 16.5; b = 3.4; x = 0.61.

  1.  

 

при х = 0.54; a = 1.1; b = - 1.22.

  1.  

 

при a = 0.7; b = 0.05; x = 0.5.

  1.  

при х = 1.82; у = 18.23;

z = 3.44.

  1.  

 

при х = 0.2; a = 1.1; b = 0.04.

  1.  

 

при a = 1.5; b = 15.6; t = 0.9.

  1.  

 

при m = 2; c = -1; t = 1.2;

b = 0.7.

  1.  

 

при b = 0.7; c = -1.8; t = 1.2.

  1.  

 

при a = 3.2;b = 17.5; x =- 4.8.

  1.  

 

при a = 3.44; b = 17.52;

 x=- 4.8,  z = 5.34.

  1.  

 

при a = 10.2; b = 9.2; x = 2.2;

c = 0.5.

  1.  

 

при х = 3.23; a = 10.23;

b = 9.84; c = 0.5.

  1.  

 

при a = 0.3; b = 0.9; x = 0.61.

  1.  

при a =0.001; b = 5.8; x = 1.77.

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

  1.  Названия лабораторной работы.
  2.  Цели и содержания лабораторной работы.
  3.  Ответов на контрольные вопросы лабораторной работы.
  4.  Формулировки индивидуального задания и порядка его выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

  1.  Назовите преимущества языка С++.
  2.  Какие окна включает в себя рабочий стол Visual C++?
  3.  Каковы этапы создания консольного приложения в среде Visual C++?
  4.  Какова структура программы, написанной на языке C++?
  5.  Какие существуют основные типы данных в языке C++? Дать их назначение.
  6.  Каково назначение директивы #include в языке C++?
  7.  Каково назначение функций printf() и puts() в языке C++?
  8.  Каково назначение функций clrscr(), getch() и cputs() в языке C++?
  9.  Каким образом осуществляется потоковый ввод-вывод в языке C++? Каково назначение объектов cin и cout?
  10.  Что такое литерал?
  11.  Как производится объявление переменной и константной переменной в языке C++? В чем отличие присваивания от инициализации?
  12.  Какие спецификаторы используются для задания класса памяти?
  13.  Какое существует деление операций в соответствии с количеством операндов, которые в них используются?
  14.  Из чего состоят выражения?
  15.  Какие существуют математические функции в языке C++?
  16.  Каким образом осуществляется диагностирование ошибок компиляции в среде Visual C++?

Пример выполнения лабораторной работы №1:

1. Формулировка индивидуального задания:

Вычислить: 

при х = 1.7; a = 0.5; b =1.08; c = 2.1; m =0.7.

2. Листинг программы:

// Лабораторная работа №1

/* Горошко А.А.

гр. БАС-051*/

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

double x, z, s;

//const double a = 0.5, b = 1.08, c = 2.1, m = 0.7;

double a, b, c, m;

 

cout<<"Введите переменную"<<"\nx=";

   cin >> x;

 cout<<"Введите переменную"<<"\na=";

   cin >> a;

 cout<<"Введите переменную"<<"\nb=";

   cin >> b;

 cout<<"Введите переменную"<<"\nc=";

   cin >> c;

 cout<<"Введите переменную"<<"\nm=";

   cin >> m;

 z=(sin(x)/sqrt(1+m*m*sin(x*x))-c*m*log10(m*x));

cout<< "z=";

cout<< z;

s=(exp(-a*x)*sqrt(x+1)+exp(-b*x)*sqrt(x+1.5));

cout<< "\ns=";

cout<< s;

 

getch();

 return 0;

}

3. Результаты работы программы:

Лабораторная работа №2.
Программирование алгоритмов разветвляющейся структуры
в языке  C++

Цель работы и содержание: приобретение навыков программирования разветвляющихся алгоритмов. Освоить операторы языка C++ if и switch, позволяющего реализовывать разветвляющиеся алгоритмы.

Ход работы

Диаграммы деятельности. Унифицированный язык моделирования (UML) является стандартным инструментом для создания «чертежей» программного обеспечения. С помощью UML можно визуализировать, специфицировать, конструировать и документировать артефакты программных систем. UML пригоден для моделирования любых систем: от информационных систем масштаба предприятия до распределенных Web-приложений и даже встроенных систем реального времени. Это очень выразительный язык, позволяющий рассмотреть систему со всех точек зрения, имеющих отношение к ее разработке и последующему развертыванию. Несмотря на обилие выразительных возможностей, этот язык прост для понимания и использования.

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

Диаграмма деятельности (Activity diagram) показывает поток переходов от одной деятельности к другой. Деятельность (Activity) - это продолжающийся во времени неатомарный шаг вычислений в автомате. Деятельности в конечном счете приводят к выполнению некоего действия (Action), составленного из выполняемых атомарных вычислений, каждое из которых либо изменяет состояние системы, либо возвращает какое-то значение. Действие может заключаться в вызове другой операции, посылке сигнала, создании или уничтожении объекта либо в простом вычислении - скажем, значения выражения. Графически диаграмма деятельности представляется в виде графа, имеющего вершины и ребра.

Состояние действия и состояние деятельности. В потоке управления, моделируемом диаграммой деятельности, происходят различные события. Вы можете вычислить выражение, в результате чего изменяется значение некоторого атрибута или возвращается некоторое значение. Также, например, можно выполнить операцию над объектом, послать ему сигнал  или даже создать его или уничтожить. Все эти выполняемые атомарные вычисления называются состояниями действия, поскольку каждое из них есть состояние системы, представляющее собой выполнение некоторого действия. Как показано на рис. 2.1, состояния действия изображаются прямоугольниками с закругленными краями. Внутри такого символа можно записывать произвольное выражение.

а)

б)

в)

Рисунок 2.1 - Состояния действия и комментарий: а) простое действие;

б) выражение; в) комментарий

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

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

Переходы. Когда действие или деятельность в некотором состоянии завершается, поток управления сразу переходит в следующее состояние действия или деятельности. Для описания этого потока используются переходы, показывающие путь из одного состояния действия или деятельности в другое. В UML переход представляется простой линией со стрелкой, как показано на рис. 2.2.

Рисунок 2.2 - Нетриггерные переходы

Поток управления должен где-то начинаться и заканчиваться (разумеется, если это не бесконечный поток, у которого есть начало, но нет конца). Как показано на рисунке, вы можете задать как начальное состояние (закрашенный кружок), так и конечное (закрашенный кружок внутри окружности).

Ветвление. Простые последовательные переходы встречаются наиболее часто, но их одних недостаточно для моделирования любого потока управления. Как и в блок-схеме, вы можете включить в модель ветвление, которое описывает различные пути выполнения в зависимости от значения некоторого булевского выражения. Как видно из рис. 2.3, точка ветвления представляется ромбом. В точку ветвления может входить ровно один переход, а выходить - два или более. Для каждого исходящего перехода задается булевское выражение, которое вычисляется только один раз при входе в точку ветвления. Ни для каких двух исходящих переходов эти сторожевые условия не должны одновременно принимать значение «истина», иначе поток управления окажется неоднозначным. Но эти условия должны покрывать все возможные варианты, иначе поток остановится.

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

Реализовать итерацию можно, если ввести два состояния действия - в первом устанавливается значение счетчика, во втором оно увеличивается - и точку ветвления, вычисление в которой показывает, следует ли прекратить итерации.

Рисунок 2.3 - Ветвление

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

Любое выражение, завершающееся точкой с занятой, рассматривается как оператор, выполнение которого заключается в вычислении выражения. Частным случаем выражения является пустой оператор ; (он используется, когда но синтаксису оператор требуется, а по смыслу - нет). Примеры:

i++;         // выполняется операция инкремента

а* = b + с;  // выполняется умножение с присваиванием

fun(i, k);   // выполняется вызов функции

Логическое выражение - некоторое утверждение, относительно которого можно сказать: истинно оно или ложно.

В языке C++  любое выражение, равное нулю считается ложным, тогда как любое выражение не равное нулю будет истинным.

В Си++ используются шесть операторов отношения (табл. 2.1), позволяющих сравнивать между собой значения числовых переменных, а также значение переменной и константы.

Условия, которые составлены с использованием одного оператора сравнения, называются простыми условиями. Общий вид:

<выражение>  <оператор_сравнения>  <выражение>

Из простых условий, которые являются выражениями логического типа можно строить составные условия.  В этом случае простые условия необходимо связывать при помощи логических операций: !(не), &&(и), ||(или).

Таблица 2.1 - Операторы отношения в языке C++

a = b

равно

a == b

a > b

больше

a > b

a < b

меньше

a < b

a ≥ b

больше или равно

a >= b

a ≤ b

меньше или равно

a <= b

a ≠ b

не равно

a != b

Операция логического И (&&). Логическая операция И используется в том случае, когда требуется одновременность выполнения двух условий, входящих в данную операцию. В языке C++ данное выражение может быть представлено следующим образом

<условие 1> && <условие 2>

На практике эта логическая операция часто применяется для создания условия, что некоторая переменная принадлежит указанному промежутку. Например, составим условие, которое будет истинно тогда и только тогда, когда переменная х принадлежит промежутку от 10 до 20: 10 < x < 20. Это произойдёт только в том случае, когда х >10 и  x < 20. На языке C++ это записывается так:

x > 10 && x < 20

Операция логического ИЛИ (||). Логическая операция ИЛИ используется только в том случае, когда требуется выполнение хотя бы одного из условий, входящих в данную операцию. В языке C++ данное выражение может быть представлено следующим образом:

<условие 1> || <условие 2>

На практике эта логическая операция часто применяется для создания условия, что некоторая переменная принимает одно из допустимых значений. Например, составим условие, которое истинно тогда и только тогда, когда x принимает фиксированные значения: 1,2; 2,3; 3,4. Это произойдет только в том случае, когда x = 1,2 или x = 2,3 или x = 3,4. На языке C++ это запишется как

x == 1.2 || x == 2.3 || x == 3.4

Операция логического НЕ (!). Операция НЕ принимает значение истина, если ее операнд принимает значение ложь, и наоборот, результатом операции НЕ будет ложь, если ее операнд принимает значение истина. В языке C++ данное выражение может быть представлено следующим образом:

! <условие>

На практике операция НЕ применяется для отрицания какого-либо факта.

Все логические операции (за исключением операции логического НЕ) имеют приоритет ниже операций отношения, поэтому обрамлять скобками простые условия на основе операторов отношения не обязательно.

Примеры:

if (a<0) b = 1;                               // 1                                              

if (a<b && (a>d||a==0)) b++;

else

{

b*=a;

a=0;

}                                              // 2                              

if (a<b)

{

if (a<c) m = a;

else m = c;

}

else

{

if (b<c) m = b;

else m = c;

}                                              // 3

if (a++) b++;                               // 4                                           

if (b>a) max = b;

else max = a;                                  // 5                                      

В примере 1 отсутствует ветвь else. Подобная конструкция называется «пропуск оператора», поскольку присваивание либо выполняется, либо пропускается в зависимости от выполнения условия.

Если требуется проверить несколько условий, их объединяют знаками логических операций. Например, выражение в примере 2 будет истинно в том случае, если выполнится одновременно условие a<b и одно из условий в скобках. Если опустить внутренние скобки, будет выполнено сначала логическое И, а потом - ИЛИ.

Оператор в примере 3 вычисляет наименьшее значение из трех переменных. Фигурные скобки в данном случае не обязательны, так как компилятор относит часть else к ближайшему if.

Пример 4 напоминает о том, что хотя в качестве выражений в операторе if чаще всего используются операции отношения, это не обязательно.

Конструкции, подобные оператору в примере 5, проще и нагляднее записывать в виде условной операции (в данном случае: max=(b>a)?b:a;).

Условный оператор.  Условный оператор if используется для разветвления процесса вычислений на два направления.

true                            false                                                   false                          

                                                                    true

Рисунок 2.4 - Структурная схема условного оператора

Сначала вычисляется выражение, которое может иметь арифметический тип или тип указателя. Если оно не равно нулю (имеет значение true), выполняется первый оператор, иначе - второй. После этого управление передается на оператор, следующий за условным.

Одна из ветвей может отсутствовать, логичнее опускать вторую ветвь вместе с ключевым словом else. Если в какой-либо ветви требуется выполнить несколько операторов, их необходимо заключить в блок, иначе компилятор не сможет понять, где заканчивается ветвление. Блок может содержать любые операторы, в том числе описания и другие условные операторы (но не может состоять из одних описаний). Необходимо учитывать, что переменная, описанная в блоке, вне блока не существует.

Условный оператор в языке C++ имеет формат:

if(<Условие>) <Оператор>;

его действие можно описать с помощью фрагмента UML-диаграммы деятельности, изображенного на рис. 2.5 а:

а)

б)

Рисунок 2.5 - Фрагмент диаграммы деятельности UML, описывающей действия оператора if в языке C++: а) без альтернативной ветви else;

б) с альтернативной ветвью else

Если помимо тех действий, которые требуется выполнить, когда заданное условие истинно, требуется исполнить и ряд действий, когда заданное условие ложно, то применяют следующую форму условного оператора:

if(<Условие>) <Оператор_1>; else <Оператор_2>;

его действие можно описать с помощью фрагмента блок-схемы алгоритма, изображенного на рис. 2.5 б. Иными словами, если «условие» принимает значение «истина», то выполняется «оператор1», и если «условие» принимает значение «ложь», то выполняется «оператор2».

Например, для оператора if без альтернативной ветви else можно привести следующий фрагмент кода:

// Условный оператор

if(temperature >= 0)

 cout << "Выше  точки  замерзания!\n";

// Действия, не относящиеся к условному оператору

cout << "Температура " << temperature << endl;

тогда как для оператора if-else можно привести следующий пример:

// Условный оператор

if(x < у) min = х;

else min = у;

// Действия, не относящиеся к условному оператору

cout << "min = " << min;

Последний пример может быть прокомментирован следующим образом. Если условие x < у истинно, то переменной min будет присвоено значение х, если оно ложно, то min будет присвоено значение у. После выполнения инструкции  if-else будет напечатано значение min.

Оба оператора <Оператор_1> и <Оператор_2> могут представлять простые операторы (один оператор), в этом случае они не заключаются в фигурные скобки. Если же <Оператор_1> и/или <Оператор_2> представляют составной оператор (несколько операторов), то их нужно заключить в фигурные скобки.

{

Оператор_1;

Оператор_2;

...

Оператор_n;

}

Каждый оператор внутри скобок должен заканчиваться точкой с запятой.

Структура называется вложенной, если после <условия> или служебного слова else используются вновь условные операторы. Число вложений может быть произвольным. При этом справедливо следующее правило: служебное слово else всегда относится к ближайшему выше <условию>.

Пример 2.1. Составить UML-диаграмму деятельности и программу с использованием конструкции ветвления для вычисления значения функции:

(3.1)

Составим UML-диаграмму деятельности вычисления значения функции (рис. 2.6).

Рисунок 2.6 - UML-диаграмма деятельности для задачи расчета значения функции

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

Листинг 2.1

#include “stdafx.h”

#include <iostream>

#include <math.h>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

double x, y;

 

 setlocale( LC_ALL, "Russian" );

 cout << ”Введите x: ”;

 cin >> x;

if (x <= 0)

 y=2*x*x + cos(x);

else

 if (x<5) y = x + 1;

 else y = sin(x) - x*x;

cout << “y(x = ” << x << “) = ” << y << endl;

getch();

 return 0;

}

Переключатель. Оператор switch (переключатель) предназначен для разветвления процесса вычислений на несколько направлений. Переключатель является наиболее удобным средством для организации мультиветвления. Синтаксис переключателя таков:

switch(Выражение)

{

case Константа_1: Операторы_1;

case Константа_2: Операторы_2;

...

case Константа_n: Операторы_n;

default: Операторы_(n+1);

}

Рисунок 2.7 - Структурная схема оператора switch

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

Выход из переключателя обычно выполняется с помощью операторов break или return. Оператор break выполняет выход из самого внутреннего из объемлющих.

Управляющая конструкция switch передает управление к тому из помеченных с помощью case операторов, для которого значение константного выражения совпадает со значением переключающего выражения. Переключающее выражение должно быть целочисленным или его значение приводится к целому. Значения константных выражений, помещаемых за служебными словами case, приводятся к типу переключающего выражения. В одном переключателе все константные выражения должны иметь различные значения, но быть одного типа. Любой из операторов, помещенных в фигурных скобках после конструкции switch(...), может быть помечен одной или несколькими метками вида

case константное_выражение:

Если значение переключающего выражения не совпадает ни с одним из константных выражений, то выполняется переход к оператору, отмеченному меткой default:. В каждом переключателе должно быть не больше одной метки default, однако эта метка может и отсутствовать. В случае отсутствия метки default при несовпадении переключающего выражения ни с одним из константных выражений, помещаемых вслед за case, в переключателе не выполняется ни один из операторов.

Сами по себе метки case и default не изменяют последовательности выполнения операторов. Если не предусмотрены переходы или выход из переключателя, то в нем последовательно выполняются все операторы, начиная с той метки, на которую передано управление. Фрагмент UML-диаграммы деятельности, соответствующий переключателю, изображен на рис. 2.8.

Пример 2.2. Написать программу вывода нечетных чисел, не меньших чем введенное с клавиатуры число от 0 до 9.

Листинг 2.2

// Названия нечетных целых цифр не меньше заданной

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

 int ic;

 setlocale( LC_ALL, "Russian" );

 cout << “\nВведите любую десятичную цифру: ”;

 cin >> ic;

cout << endl;

switch(ic)

{

 case 0: case 1: cout << “один, ”;

 case 2: case 3: cout << “три, ”;

 case 4: case 5: cout << “пять, ”;

 case 6: case 7: cout << “семь, ”;

 case 8: case 9: cout << “девять. ”;

 // Выход из переключателя

 break;

 default: cout << “Ошибка! Это не цифра!\n

   // Конец переключателя

}

 getch();

 return 0;

}

В результате двух выполнений программы:

Введите любую десятичную цифру: 4 <Enter>

пять, семь, девять.

Введите любую десятичную цифру: z <Enter>

Ошибка! Это не цифра!

Рисунок 2.8 - Фрагмент диаграммы деятельности UML, описывающей действие оператора switch в языке C++

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

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

Совместно с оператором break синтаксис переключателя имеет следующий вид:

switch(Выражение)

{

case Константа_1: Операторы_1; break;

case Константа_2: Операторы_2; break;

...

case Константа_n: Операторы_n; break;

default: Операторы_(n+1);

}

   

его действия можно описать с помощью фрагмента блок-схемы алгоритма, изображенного на рис. 2.9.

Рисунок 2.9 - Фрагмент диаграммы деятельности UML, описывающей действие оператора switch совместно с оператором break в языке C++

Пример 2.3. Составить блок-схему алгоритма и программу с использованием переключателя и вывести наименование времени года по вводимому с клавиатуры номеру месяца от 1 до 12.

Составим UML-диаграмму деятельности определения наименования времени года по вводимому с клавиатуры номеру месяца (рис. 2.10).

Рисунок 2.10 - UML-диаграмма деятельности для задачи вывода названия времени года по номеру месяца

На UML-диаграммах деятельности логическое отношение И в условии обозначается с помощью связки and, логическое отношение ИЛИ - с помощью связки or, а логическое отрицание НЕ - c помощью связки not.

По составленной диаграмме может быть написана программа решения поставленной задачи.

Листинг 2.3

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

 int n;

 setlocale( LC_ALL, "Russian" );

 cout << "Введите номер месяца: ";

 cin >> n;

 cout<<"\nВремя года: ";

 switch(n)

{

 case 1:case 2:case 12: cout << "Зима\n";

 break;

  case 3:case 4:case 5: cout << "Весна\n";

 break;

  case 6:case 7:case 8: cout << "Лето\n";

 break;

 case 9:case 10:case 11: cout << "Осень\n";

 break;

  default: cout << "\Ошибка!\n";

 }

 getch();

 return 0;

}

В приведенном примере программы при вводе номера месяца от 1 до 12 на экране печатается соответствующее время года, если же номер месяца превышает 12, выводится сообщение о неверном вводе месяца, для чего служит зарезервированное слово языка default. После каждой из констант, перечисляющих значение переключателя, ставится двоеточие.

Заключительный для каждой ветви оператор break служит для прерывания цикла проверки и перехода в конец переключателя. В случае отсутствия break  переключатель работает неверно - происходит переход на следующую ветвь.

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

  1.  Проработать примеры, приведенные в лабораторной работе.
  2.  Для программы из примера 2 составить соответствующую ей диаграмму деятельности UML.
  3.  Составить UML-диаграмму деятельности и программу с использованием конструкции ветвления и вычислить значение функции. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №1. Вариант:

  1.  где ;
  2.  где ;
  3.  
  4.  где ;
  5.    где ;
  6.  
  7.  где ;
  8.  
  9.   где ;
  10.  
  11.   где ;
  12.   где ;
  13.  
  14.  
  15.  
  16.  
  17.  
  18.  
  19.  
  20.  
  21.  
  22.  
  23.  
  24.  
  25.  

  1.  Решить задачу согласно варианта, составить UML-диаграмму деятельности и программу с использованием переключателя. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №2. Вариант:

  1.  Дано натуральное число n > 100. Вывести на экран фразу «Мне n лет», учитывая, что при некоторых значениях n слово «лет» надо заменить на слово «год» или «года».
  2.  Дано число m (1  m  12).Определить, сколько дней в месяце с номером m.
  3.  Дано число m (1  m  7). Вывести на экран название дня недели, который соответствует этому номеру.
  4.  С клавиатуры вводится натуральное число n. В зависимости от значения остатка r при делении числа n на 7 вывести на экран число n в виде n = 7*k + r.
  5.  С клавиатуры вводится цифра m (от 1 до 4). Вывести на экран названия месяцев, соответствующих времени года с номером m (считать зиму временем года № 1).
  6.  Дано целое число С такое, что |С| < 9. Вывести это число в словесной форме, учитывая его знак.
  7.  С клавиатуры вводится цифра m (от 1 до 12). Вывести на экран название месяца, соответствующего цифре,
  8.  Дано число m (1 m  12). Определить полугодие, на которое приходится месяц с номером m и количество дней в том месяце (год не високосный).
  9.  Вводится число экзаменов N<=20. Напечатать  фразу  "Мы  успешно сдали N экзаменов", согласовав слово "экзамен" с числом  N.
  10.  Вводится число карандашей N<=10. Вывести фразу "Я купил  N  карандашей, согласовав слово "карандаш" с числом N.
  11.  Компания по снабжению электроэнергией взимает плату с клиентов по тарифу:
  •  7 р. за 1 кВт/ч за первые 250 кВт/ч;
  •  17 р. за кВт/ч, если потребление свыше 250, но не превышает 300 кВт/ч;
  •  20 р. за кВт/ч, если потребление свыше 300 кВт/ч.  

    Потребитель израсходовал n кВт/ч. Подсчитать плату.

  1.  При покупке товара на сумму от 200 до 500 руб предоставляется скидка 3%, при покупке товара на сумму от 500 до 800 - скидка 5%, при покупке товара на сумму от 800 до 1000 руб - скидка 7%,  свыше 1000 руб - скидка 9%. Покупатель приобрел 8 рулонов обоев по цене Х1 и две банки краски по цене Х2. Сколько он заплатил?
  2.  Студенты убирают урожай помидоров. При сборе до 50 кг в день работа оплачивается из расчёта 30 коп. за 1 кг; при сборе от 50 до 75 кг в день - 50 коп. за 1 кг; при сборе от 75 до 90  кг в день - 65 коп. за 1 кг; при сборе свыше 90 кг в день - 70 коп. за 1 кг плюс 20 руб. премия. Студент собрал X кг за день. Определить его заработок.
  3.  Решить задачу согласно варианта, составить UML-диаграмму деятельности и программу с использованием конструкций ветвления и  переключателя. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №3. Вариант:

  1.  В японском календаре был принят 60-летний цикл, состоящий из пяти 12-летных подциклов. Внутри подцикла года носили названия животных мыши, коровы, тигра, зайца, дракона, змеи, лошади, овцы, обезьяны, курицы, собаки и свиньи. Попарно года в подцикле обозначались названиями цвета: зеленый, красный, желтый, белый и черный. По номеру года определить его название по японскому календарю, считая за начало очередного цикла 1984 год - год зеленой мыши (1985 - год зеленой коровы,  1986 - год красного тигра, 1987 - год красного зайца и т.д.).
  2.  Даны действительные числа х и у. Найти U = max2(x2y, xy2) + min2(x-y, x+2y).
  3.  Из трех действительных чисел a, b и c выбрать те, модули которых не меньше 4.
  4.  Напечатать три данных действительных числа a, b и c сначала в порядке их возрастания, затем - в порядке убывания.
  5.  Определить принадлежит ли точка А(a, b) кольцу определяемому окружностями х2 + у2 = 1 и х2 + у2 = 0.25.
  6.  Решить квадратное неравенство ax2 + bx + c > 0 (а 0), где  a, b и с - действительные числа.
  7.  Провести исследование биквадратного уравнения ax4 + bx2 + c = 0 (а 0), где  a, b и с - действительные числа. Если действительных корней нет, то об этом должно быть выдано сообщение, иначе должны быть выданы 2 или 4 действительных корня.
  8.  Решить неравенство , где а - произвольное действительное числа.
  9.  Найти координаты точки пересечения прямых заданных уравнениями a1x + b1y + c1 = 0 и a2x + b2y + c2 = 0, либо сообщить совпадают, параллельны или не существуют.
  10.  Вывести на экран большее из трёх заданных чисел.
  11.  Определить, есть ли среди трёх заданных чисел чётные.
  12.  Две окружности заданы координатами центра и радиусами. Сколько точек пересечения имеют эти окружности?
  13.  Составить программу, выясняющую делится ли натуральное число х нацело на натуральное число у.
  14.  Составить программу нахождения из трех чисел наибольшего и наименьшего.
  15.  Составить программу решения квадратного уравнения.
  16.  Даны три действительных числа. Составить программу, выбирающую из них те, которые принадлежат интервалу (0,1).
  17.  Определить, есть ли среди трёх заданных чисел нечётные.
  18.  Даны произвольные действительные числа a, b и с. Вывести на экран сообщения: треугольник с данными длинами сторон построить можно (указать равнобедренный, равносторонний или разносторонний получится треугольник), либо треугольник с данными длинами сторон построить нельзя.
  19.  Какая из точек A(a1, a2) или B(b1,b2) находится дальше от начала координат?
  20.  Попадёт ли точка А(a1, a2) в окружность заданного радиуса с центром в начале координат?
  21.  Симметричны ли точки M1(x1, y1) и M2(x2, y2) относительно начала координат?
  22.  Треугольник задан координатами своих вершин. Определить принадлежит ли данная точка треугольнику. Координаты вершин треугольника и координаты точки задать самостоятельно.
  23.  Симметричны ли точки M1(x1, y1) и M2(x2, y2) относительно оси Ох или относительно оси Оу?

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

  1.  Названия лабораторной работы.
    1.  Цели и содержания лабораторной работы.
    2.  Ответов на контрольные вопросы лабораторной работы.
    3.  UML-диаграмму деятельности для программы примера 2 лабораторной работы.
    4.  Формулировки индивидуальных заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

  1.  Для чего нужны диаграммы деятельности UML?
  2.  Что такое состояние действия и состояние деятельности?
  3.  Какие нотации существуют для обозначения переходов и ветвлений в диаграммах деятельности?
  4.  Какой алгоритм является алгоритмом разветвляющейся структуры?
  5.  Чем отличается разветвляющийся алгоритм от линейного?
  6.  Что такое условный оператор? Какие существуют его формы?
  7.  Что такое составной оператор? Каков формат его записи?
  8.  Какие операторы сравнения используются в Си?
  9.  Что называется простым условием? Приведите примеры.
  10.  Что такое составное условие? Приведите примеры.
  11.  Какие логические операторы допускаются при составлении сложных условий?
  12.  Может ли оператор ветвления содержать внутри себя другие ветвления?
  13.  Что такое множественный выбор?
  14.  В каких случаях применяется переключатель?
  15.  Зачем ставится в переключателе оператор break?
  16.  Зачем в переключателе употребляется зарезервированное слово default?
  17.  Какие UML-диаграммы показывают работу переключателя?

Пример выполнения лабораторной работы №2:

1. Индивидуальное задание №1:

1.1. Постановка задачи:

Составить UML-диаграмму деятельности и программу с использованием конструкции ветвления и вычислить значение функции

1.2. UML-диаграмма:

1.3. Листинг программы:

// // Лабораторная работа №2

/* Горошко А.А.

гр. БАС-051*/

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

double x, y;

setlocale( LC_ALL, "Russian" );

cout<<Введите х<<"\nx=";

 cin>> x;

 if (x > 3.5)

 y=(log(x)*cos(2*x));

else

 if (x=3.5) y=(pow(x*x*x-1,1/3));

 else y=(1+sin(x)*sin(x)-2*cos(2*x)*cos(2*x));

cout<< "y=";

cout<< y;

getch();

 return 0;

}

1.4. Результаты работы программы:

          

2. Индивидуальное задание №2:

2.1. Постановка задачи:

Решить задачу, составить UML-диаграмму деятельности и программу с использованием переключателя.

Задача: в понедельник фирма работает с 9-00 до 16-00; во вторник, среду, четверг, пятницу - с 8-00 до 19-00; в субботу - с 10-00 до 15-00; воскресенье - выходной. По заданному номеру дня недели определить часы работы.

2.2. UML-диаграмма:

 2.3. Листинг программы:

// Индивидуальное задание №2

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

int n;

setlocale( LC_ALL, "Russian" );

cout<<"Введите номер дня недели\n"<<"\nn=";

 cin>> n;

 

switch(n)

{

 case 1:

  cout<<"\nВремя работы фирмы: с 9-00 до 16-00\n";

  break;

 case 2:case 3:case 4:case 5:

  cout<<"\nВремя работы фирмы: с 8-00 до 19-00\n";

  break;

 case 6:

  cout<<"\nВремя работы фирмы: с 10-00 до 15-00\n";

  break;

 case 7:

  cout<<"\nВыходной\n";

  break;

 default:

  cout<<"\nОшибка!\n";

}

 

getch();

return 0;

}

2.4. Результаты работы программы:

3. Индивидуальное задание №3:

3.1. Постановка задачи:

Решить задачу, составить UML-диаграмму деятельности и программу с использованием конструкций ветвления и  переключателя.

Задача: школьники сдают нормы по прыжкам в длину. Если длина прыжка больше 2,5 м, то оценка - 5, если от 2 м до 2,5 -  оценка 4; от 1,5 м до 2 м - оценка 3; если меньше 1,5 м - 2. Выставить школьнику оценку, если известна длина его прыжка.

3.2. UML-диаграмма:

3.3. Листинг программы:

// Индивидуальное задание №3

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

double dlina;

setlocale( LC_ALL, "Russian" );

cout<<"Введите длину прыжка\n"<<"\ndlina=";

 cin>> dlina;

if (dlina > 2.5)cout<<"\nОценка: 5\n";

else

 if (dlina > 2)cout<<"\nОценка: 4\n";

 else

  if (dlina > 1.5)cout<<"\nОценка: 3\n";

  else

   if (dlina > 0)cout<<"\nОценка: 2\n";

   else cout<<"\nОшибка! Введите положительное число\n";

 

getch();

return 0;

}

3.4. Результаты работы программы:

     


Лабораторная работа №3.
Организация циклических вычислений в языке  C++

Цель работы и содержание: приобретение навыков программирования циклических алгоритмов. Освоить конструкции языка C++, позволяющего реализовывать циклические алгоритмы.

Ход работы

Алгоритм циклической структуры - это алгоритм, в котором происходит многократное повторение одного и того же участка программы. Такие повторяемые участки вычислительного процесса называются циклами.

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

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

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

Программа циклической структуры содержит один или несколько циклов. Различают детерминированные циклы с заранее известным числом повторений и итерационные циклы, в которых число повторений заранее неизвестно.

В языке C++ существует 3 вида циклов:

  1.  цикл с параметром или цикл типа for,
  2.  цикл с предусловием или цикл типа while,
  3.  цикл с постусловием или цикл типа do ... while.

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

Цикл завершается, если условие его продолжения не выполняется. Возможно принудительное завершение, как текущей итерации, так и цикла в целом. Для этого служат операторы break, continue, return и goto.

Цикл с параметром или цикл типа for. Для цикла типа for заголовок цикла состоит из трех разделов: инициализации (присваивания начальных значений), проверки условия повторения, модификации (изменения параметров). Разделителем между разделами заголовка цикла типа for служит точка с запятой.

Рисунок 3.1 - Структурная схема оператора цикла с параметром (for)

Основная форма цикла for имеет следующий вид:

for(<список_инициализации>; <условие_продолжения>;

<переход_к_следующей_итерации>) <тело_цикла>;

Все три параметра цикла for должны быть разделены точкой с запятой. Перед выполнением цикла выполняются операторы, содержащиеся в первом параметре <список_инициализации> оператора for,  затем осуществляется проверка условия продолжения цикла (обычное условное выражение, определяющее, при каких условиях цикл будет продолжен) в параметре  <условие_продолжения>, если данное условие  принимает истинное значение, то выполняются операторы, образующие тело цикла (ело_цикла>). Изменение (приращение) обычно используется для изменения параметра цикла каждый раз при повторении цикла. Если условие продолжения принимает ложное значение,  то работа цикла завершается, и выполняются операторы, следующие за телом цикла. После выполнения последнего оператора в теле цикла, выполняются <переход к следующей итерации> операторы  третьего параметра цикла for. После чего снова производится проверка условия продолжения и процесс повторяется.

Инициализация используется для объявления и присвоения начальных значений величинам, используемым в цикле. В этой части можно записать несколько операторов, разделенных занятой (операцией «последовательное выполнение»). Областью действия переменных, объявленных в части инициализации цикла, является цикл. Инициализация выполняется один раз в начале исполнения цикла.

Выражение определяет условие выполнения цикла: если его результат, приведенный к типу bool, равен true, цикл выполняется. Цикл с параметром реализован как цикл с предусловием.

Модификации выполняются после каждой итерации цикла и служат обычно для изменения параметров цикла. В части модификаций можно записать несколько операторов через запятую. Простой или составной оператор представляет собой тело цикла. Любая из частей оператора for может быть опущена (но точки с запятой надо оставить на своих местах!).

Рисунок 3.2 - Фрагмент диаграммы деятельности UML, описывающей действия оператора for в C++

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

unsignet char ch;

for(ch = ‘A’; ch <= ‘Z’; ch++) cout << ch;

Cледующий  фрагмент программы показывает как вывести на печать квадраты вещественных чисел от -2 до 3 с шагом 0.1:

double x;

for(x = -2.; x <= 3.; x += 0.1)

cout << x*x << endl;  

С помощью цикла типа for удобно находить суммы, произведения, искать максимальные и минимальные значения и т.п. При нахождении суммы некоторой переменной, например S присваивается значение 0, затем в цикле к этой переменной прибавляется соответствующий член заданной последовательности. Далее показано, как можно найти сумму натуральных чисел от 1 до 10:

S = 0;

for(i = 1; i < 11; i++) S += i;

с использованием операции «запятая» и операции постфиксного инкремента данная задача может быть решена следующим образом:

for(S = 0, i = 1; i < 11; S += i++);

Цикл for может быть вложенным. В качестве примера рассмотрим программу, печатающую таблицу умножения целых чисел от 0 до 9:

Листинг 3.1

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

int i, j;

for(i = 0; i < 10; i++)

 for(j = 0; j < 10; j++)

  cout << i << “*“ j << “ = “<< i*j << endl;

  

getch();

return 0;

}

Пример 3.1. Составить UML-диаграмму деятельности и написать программу, позволяющую вычислить сумму квадратов натуральных чисел от 1 до n, где n вводится с клавиатуры.

S = = 12 + 2 2  + . . . + n 2.

(3.1)

Составим UML-диаграмму деятельности расчета суммы квадратов натуральных чисел от 1 до n (рис. 3.3).

Рисунок 3.3 - UML-диаграмма деятельности для задачи расчета суммы квадратов натуральных чисел от 1 до n

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

Листинг 3.2

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

int n, i, S = 0;

setlocale( LC_ALL, "Russian" );

 cout << “Введите значение n: “;

cin >> n;

for(i = 1; i <= n; i++) S += i*i;

cout << “Сумма чисел S = “ << S << endl;

getch();

 return 0;

}

Пример 3.2. Составить UML-диаграмму деятельности и написать программу, позволяющую вычислить конечную сумму

(3.2)

где n и x вводятся с клавиатуры.

Составим UML-диаграмму деятельности расчета конечной суммы (рис. 3.4).

Рисунок 3.4 - UML-диаграмма деятельности для задачи расчета конечной суммы

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

Листинг 3.3

#include “stdafx.h”

#include <iostream>

#include <math.h>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

double x, S = 0.;

int k, n;

 setlocale( LC_ALL, "Russian" );

cout << “Введите значение x: “;

 cin >> x;

 cout << “Введите значение n: “;

 cin >> n;

 for(k = 1, k <= n; k++)

{

 double a = log(k*x)/(k*k);

 S += a;

}

cout << “Конечная сумма: “ << S << endl;

getch();

 return 0;

}

Очень часто в операторе for используются операторы инкремента (++) - увеличения на единицу и декремента (--) - уменьшения на единицу. Оба оператора используются как в префиксной, так и в постфиксной формах. Префиксная операция инкремента (++i) - увеличение на 1 операнда до его использования, соответственно, префиксная операция декремента (--i) - уменьшение на 1 операнда до его использования. Постфиксная операция инкремента (i++) - увеличение значения операнда на 1 после его использования, соответственно, постфиксная операция декремента (i--) - уменьшение значения операнда на 1 после его использования. Операнд этих операций не может быть константой либо другим праводопустимым выражением. Операндом не может быть и произвольное выражение. Операндом унарных операций ++ и -- должны быть всегда леводопустимые выражения, например, переменные.

Пример 3.3. Составить UML-диаграмму деятельности и написать программу, позволяющую протабулировать функцию, заданную формулой (2.1), в диапазоне от  до  в  равноудаленных точках.

Составим UML-диаграмму деятельности расчета значений функции в указанных точках (рис. 3.5).

Рисунок 3.5 - UML-диаграмма деятельности для задачи табулирования функции

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

Листинг 3.4

#include “stdafx.h”

#include <iostream>

#include <math.h>

using namespace std;

const double x_min = -10.;

const double x_max = 10.;

const int N = 100;

int _tmain(int argc, _TCHAR* argv[])

{

double y, dx = (x_max - x_min) / N;

for(double x = x_min; x <= x_max; x += dx)

{

 if(x <= 0.) y = 2*x*x + cos(x);

 else if(x < 5.) y = x + 1;

 else y = sin(2*x) - x*x;

 cout << “x = “ << x << “ y = “ << y << endl;  

 }

 getch();

}

Любой из трех параметров цикла for может быть опущен. Например, найти сумму натуральных чисел от 1 до 10 можно с помощью следующего фрагмента программы:

 for(S = 0, i = 1; i < 11)

 {

 S += i;

 i++;

}

Если не задано условие завершения в цикле for, то он будет выполняться бесконечно. В этом случае для завершения работы цикла необходимо использовать оператор break. Для нахождения суммы чисел от 1 до 10 можно использовать также следующий фрагмент кода:

 for(S = 0, i = 1; ; i++)

{

 if(i > 10) break;

 S += i;

}

Также может быть пропущен список инициализации в цикле for, в этом случае:

 S = 0;

i = 1;

 for( ; i < 11; i++) S += i;

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

 for( ; ; ) ; \\ бесконечный цикл

 for( ; 1; ) ; \\ бесконечный цикл

Цикл с предусловием или цикл типа while. Не всегда число повторений цикла известно заранее, в этих случаях применяются циклы с предусловием (проверка перед циклом) или с постусловием (проверка после цикла) - это цикл while. Основная его форма

while(<выражение_условие>) <тело_цикла>

При входе в цикл вычисляется выражение_условие. Если его значение отлично от нуля, то выполняется тело_цикла. Затем вычисление условия, заданного в выражении_условии и выполнение операторов тела_цикла  выполняются последовательно, пока выражение_условие не станет равным 0. Данная инструкция может быть выполнена ноль или более раз. Оператором while удобно пользоваться для просмотра всевозможных последовательностей, если в конце каждой из них находится заранее известный признак.

Например, сумма натуральных чисел от 1 до 10 с использованием цикла while может быть найдена следующим образом:

 

S = 0;

i = 1;

while(i <= 10)

{

 S += i;

 i++;

 }

Рисунок 3.6 – Структурная схема оператора цикла с предусловием

Рисунок 3.7 - Фрагмент диаграммы деятельности UML, описывающей действия оператора while в C++

Пример 3.4. Найти значение натурального логарифма  от аргумента , значение которого вводится с клавиатуры с точностью , по его разложению в ряд

.

(3.3)

Чтобы вычислить сумму ряда найдем рекуррентное соотношение, позволяющее найти следующий член ряда, исходя из значения текущего. Для этого разделим следующий член ряда на текущий. Текущий член ряда задается выражением

,

(3.4)

тогда как следующий член ряда может быть определен следующим образом

.

(3.5)

Отношение следующего и текущего членов ряда

,

(3.6)

следовательно

.

(3.7)

Первый член ряда

.

(3.8)

С учетом выражения (3.8) формула (3.7) может быть записана в следующем виде

.

(3.9)

Условие завершения вычисления суммы ряда может быть записано следующим образом . На основании данных рассуждений может быть построена диаграмма деятельности UML для решения поставленной задачи (рис. 3.8)

Рисунок 3.8 - UML-диаграмма деятельности для расчета значения натурального логарифма по его разложению в ряд

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

Листинг 3.5

#include “stdafx.h”

#include <iostream>

#include <math.h>

using namespace std;

// Точность вычислений.

const double eps = 1e-10;

int _tmain(int argc, _TCHAR* argv[])

{

double x, a, a02, S;

int n = 0;

 setlocale( LC_ALL, "Russian" );

 cout << “Введите значение x: “;

 cin >> x;

// Первый член ряда.

 a = (x - 1)/(x + 1);

// Квадрат первого члена ряда.

 a02 = a*a;

// Инициализировать сумму членов ряда.

 S = a;

while(fabs(a) > eps)

{

 a *= ((n + n + 1)*a02)/(n + n + 3);

 // 2*n = n + n

 n++; S += a;

 }

 cout << “Натуральный логарифм: “ << (S + S)

 << endl 

 << “Точное значение: “ << log(x) << endl;

 getch();

 return 0;

}

Цикл с постусловием или цикл типа dowhile. Инструкция do представляет собой вариант инструкции while. Но вместо проверки условия в начале цикла, в инструкции do она производится в конце. Это значит, что инструкция, контролируемая условием do, выполняется по крайней мере один раз, тогда как while может вообще не передать управление своему телу, если условие изначально не выполняется.  Оператор цикла do … while называется оператором цикла с постусловием. Основная форма цикла do

do

<тело_цикла>

while (<выражение_условие>);

Сначала выполняется тело_цикла, затем вычисляется условие продолжения цикла выражение_условие. Если оно истинно то управление передается обратно на начало инструкции do и процесс повторяется. Когда значение условия становится ложно, управление передается следующей инструкции после тела цикла.

Например, сумма натуральных чисел от 1 до 10 с использованием цикла dowhile может быть найдена следующим образом:

S = 0;

i = 1;

do

{

S += i;

i++;

}

while(i < 11);

Рисунок 3.9 - Структурная схема оператора цикла с постусловием

Рисунок 3.10 - Фрагмент диаграммы деятельности UML, описывающей действия оператора dowhile в C++

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

.

(3.10)

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

Составим диаграмму деятельности UML для задачи отыскания значения квадратного корня по итеративной формуле (рис. 3.6), которая соответствует следующей прорамме.

Листинг 3.6

#include “stdafx.h”

#include <iostream>

#include <math.h>

using namespace std;

// Точность вычислений.

const double eps = 1e-10;

int _tmain(int argc, _TCHAR* argv[])

{

double x = 1., xp, a;

 setlocale( LC_ALL, "Russian" );

cout << “Введите значение a: “;

cin >> a;

if(a <= 0)

 {

 cout << “Неверное значение a!\n”;

 return 1;

}

 do 

 {

 xp = x; // Предыдущее значение x.

 x = (x + a/x)/2; // Текущее значение x.

 } while(fabs(x - xp) > eps)

cout << “Квадратный корень “ << x << endl <<

 “Точное значение “ << sqrt(a) << endl;

 getch();

 return 0;

}

Любой цикл while может быть приведен к эквивалентному ему циклу for и, наоборот, по следующей схеме:

for(b1; b2; b3) оператор;                    

b1;

while(b2)

{

    оператор;

    b3;

}

Часто встречающиеся ошибки при программировании циклов - использование в теле цикла неинициализированных переменных и неверная запись условия выхода из цикла.

Чтобы избежать ошибок, рекомендуется:

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

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

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

Оператор for предпочтительнее в большинстве остальных случаев (однозначно - для организации циклов со счетчиками).

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

В C++ есть четыре оператора, изменяющих естественный порядок выполнения вычислений:

  •  оператор безусловного перехода goto;
  •  оператор выхода из цикла break;
  •  оператор перехода к следующей итерации цикла continue;
  •  оператор возврата из функции return.

Оператор безусловного перехода goto. Оператор безусловного перехода имеет вид:

goto идентификатор;

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

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

goto В; // Ошибочный переход, минуя описание

float х = 0.0;   // Инициализация

                // не будет выполнена

goto В; // Допустимый переход, минуя блок

{

 int n = 10; /* Внутри блока определена

                                  переменная*/

 x = n*x + x;

}

В: cout << "\tx = " << х;

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

Принятая в настоящее время дисциплина программирования рекомендует либо вовсе отказаться от оператора goto, либо свести его применение к минимуму и строго придерживаться следующих рекомендаций:

  •  не входить внутрь блока извне;
  •  не входить внутрь условного оператора, т.е. не передавать управление операторам, размещенным после служебных слов if или else;
  •  не входить извне внутрь переключателя (switch);
  •  не передавать управление внутрь цикла.

Следование перечисленным рекомендациям позволяет исключить возможные нежелательные последствия бессистемного использования оператора безусловного перехода. Полностью отказываться от оператора goto вряд ли стоит. Есть случаи, когда этот оператор обеспечивает наиболее простые и понятные решения. Один из них - это ситуация, когда в рамках текста одной функции необходимо из разных мест переходить к одному участку программы. Если по каким-либо причинам эту часть программы нельзя оформить в виде функции (например, это может быть текст на ассемблере), то наиболее простое решение - применение безусловного перехода с помощью оператора goto. Такое положение возникает, например, при необходимости обрабатывать ошибки, выявляемые в процессе выполнения программы.

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

Оператор break. Оператор break служит для принудительного выхода из цикла или переключателя. Определение «принудительный» подчеркивает безусловность перехода. Например, в случае цикла не проверяются и не учитываются условия дальнейшего продолжения итераций. Оператор break прекращает выполнение оператора цикла или переключателя и осуществляет передачу управления (переход) к следующему за циклом или переключателем оператору. При этом в отличие от перехода с помощью goto оператор, к которому выполняется передача управления, не должен быть помечен. Оператор break нельзя использовать нигде, кроме циклов и переключателей.

Необходимость в использовании оператора break в теле цикла возникает, когда условия продолжения итераций нужно проверять не в начале итерации (циклы for, while), не в конце итерации (цикл do), а в середине тела цикла. В этом случае тело цикла может иметь такую структуру:

{

операторы;

 if(условие) break;

операторы;

}

Циклы и переключатели могут быть многократно вложенными. Однако следует помнить, что оператор break позволяет выйти только из самого внутреннего цикла или переключателя.

При многократном вложении циклов и переключателей оператор break не может вызвать передачу управления из самого внутреннего уровня непосредственно на самый внешний. Например, при решении задачи поиска в матрице хотя бы одного элемента с заданным значением удобнее всего пользоваться не оператором break, а оператором безусловной передачи управления (goto).

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

while(<условие>)

{

 if(<условие_прерывания>) continue;

}

do 

{

 if(<условие_прерывания>) continue;

} while(<условие>);

for(<список_инициализации>; <условие>;

<следующая_итерация>)

{

 if(<условие_прерывания>) continue;

}

В каждой из форм многоточием обозначены операторы тела цикла.

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

Пример 3.7. Вычислить значение специальной функции (интегральной показательной функции)

.

(3.11)

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

Чтобы вычислить сумму ряда найдем рекуррентное соотношение, позволяющее определить следующий член ряда исходя из значения текущего. Для этого разделим следующий член ряда на текущий. Текущий член ряда задается выражением

,

(3.12)

тогда как следующий член ряда может быть определен следующим образом

.

(3.13)

воспользовавшись свойством факториала  последнее выражение может быть записано следующим образом

.

(3.14)

Отношение следующего и текущего членов ряда

,

(3.15)

Следовательно

.

(3.16)

Первый член ряда

.

(3.17)

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

Листинг 3.7

#includestdafx.h

#include <iostream>

#include <math.h>

using namespace std;

// Точность вычислений.

const double eps = 1e-10;

// Постоянная Эйлера

const double euler = 0.5772156649;

int _tmain(int argc, _TCHAR* argv[])

{

double x, a, S;

 setlocale( LC_ALL, "Russian" );

cout << “Введите значение x: ”;

cin >> x;

// Начальное значение члена ряда и суммы.

a = x;

S = a;

// Найти сумму членов ряда.

for(int k = 1; fabs(a) > eps; k++)

{

 // Найти значение следующего члена ряда.

 a *= x*k/((k + 1)*(k+1));

 // Добавить полученный член к сумме.

 S += a;

}

// Вывести значение интегральной

// показательной функции.

cout << “Ei(x = “ << x << “) = “ << (euler + log(x) + S) << endl;

getch();

return 0;

}

Оператор return. Оператор возврата из функции return завершает выполнение функции и передает управление в точку ее вызова. Вид оператора:

return [ выражение ];

Выражение должно иметь скалярный тип. Если тип возвращаемого функцией значения описан как void, выражение должно отсутствовать.

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

  1.  Проработать примеры, приведенные в лабораторной работе.
  2.  Составить UML-диаграмму деятельности и программу с использованием конструкций цикла для решения задачи. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №1. Вариант:

  1.  Найти все трехзначные натуральные числа, сумма цифр которых равна их произведению.
  2.  Найти сумму целых положительных чисел, больших 20, меньших 100 и кратных 3.
  3.  Начав тренировки, спортсмен пробежал 10 км. Каждый следующий день он увеличивал дневную норму на 10% от нормы предыдущего дня. Какой суммарный путь пробежит спортсмен за 7 дней?
  4.  Через сколько дней спортсмен из задания 1 будет пробегать в день больше 15 км?
  5.  Одноклеточная амеба каждые три часа делится на 2 клетки. Определить, сколько будет клеток через 6 часов.
  6.  Напечатать таблицу соответствия между весом в фунтах и весом в кг для значений от 1 до а фунтов с шагом 1 фунт, если 1 фунт = 400 г.
  7.  Определить среди всех двузначных чисел те, которые делятся на сумму своих цифр.
  8.  Сумма цифр трехзначного числа кратна 7. Само число также делится на 7. Найти все такие числа.
  9.  Если к сумме цифр двузначного числа прибавить квадрат этой суммы, то снова получится это двузначное число. Найти все эти числа.
  10.  Сколько можно купить быков, коров и телят, платя за быка 10 р., за корову - 5 р., а за теленка - 0,5 р., если на 100 р. надо купить 100 голов скота?
  11.  Составьте программу, которая печатает таблицу умножения натуральных чисел в десятичной системе счисления.
  12.  Покупатель должен заплатить в кассу S р. У него имеются 1, 2, 5, 10, 100, 500 р. Сколько купюр разного достоинства отдаст покупатель, если он начинает платить с самых крупных.
  13.  Дано натуральное число n. Получить все его натуральные делители.
  14.  Вычислить сумму всех n-значных чисел (1<=n<=4)
  15.   Вычислить сумму всех n-значных чисел, кратных k (1<=n<=4)
  16.  Ученик выучил в первый день 5 английских слов. В каждый следующий день он выучивал на 2 слова больше, чем в предыдущий. Сколько английских слов выучит ученик в 10-ый день занятий.
  17.  Составьте программу, которая печатает таблицу сложения натуральных чисел в десятичной системе счисления.
  18.  Составить программу, выдающую 1, если заданное число - простое и 0 - в противном случае. Число называется простым, если он делится только на 1 и на само себя. Делители числа   лежат  в   интервале от  2 до корня из k, где k заданное число.
  19.  У гусей и кроликов вместе 64 лапы. Сколько могло быть кроликов и гусей (указать все сочетания, которые возможны).
  20.  Заданы три натуральных числа А, В, С, которые обозначают число, месяц и год. Найти порядковый номер даты, начиная  отсчет с начала года.
  21.  Ежемесячная стипендия студента составляет А р., а расходы на проживание превышают стипендию и составляют В р. в месяц. Рост цен ежемесячно увеличивает расходы на 3 %. Составьте программу расчета необходимой суммы денег, которую надо единовременно просить у родителей, чтобы можно было прожить учебный год (10 месяцев), используя только эти деньги и стипендию.
  22.  Составьте программу, которая по номеру дня в году выводит число и месяц в общепринятой форме (например, 33-й день года - 2 февраля).

  1.  Составить UML-диаграмму деятельности и программу для нахождения значения конечной суммы. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №2. Вариант:

  1.  Вводится целое число . .
  2.  Вводится целое число . .
  3.  Вводится целое число . .
  4.  Вводится целое число . .
  5.  Вводится целое число .    
  6.  Вводится целое число .
  7.  Вводятся целые числа  и , . .
  8.  Вводится целое число .             
  9.  Вводятся целые числа  и . .
  10.  Вводится целое число . S=.
  11.  Вводится целое число .   при  xi  = (1+i),

 y = 1/i.

  1.  Вводится целое число . ,  , .
  2.  Вводится целое число . , где .
  3.  Вводится целое число . .
  4.  Вводится целое число . .
  5.  Вводится целое число . .
  6.  Вводится целое число . .
  7.  Вводится целое число . .
  8.  Вводится целое число . .

Примечание: При вычислении факториалов необходимо результат накапливать в переменных типа double.

  1.  Составить UML-диаграмму деятельности и написать программу, позволяющую протабулировать функцию, определенную в соответствии с индивидуальным заданием №1 лабораторной работы №2, в диапазоне от  до  в  равноудаленных точках.

  1.  Составить программу и произвести вычисления  вычисление значения специальной функции по ее разложению в ряд с точностью , аргумент функции  вводится с клавиатуры. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №3. Вариант:

1. Интегральный синус

;

2. Интегральный косинус

;

3. Интегральный гиперболический синус

;

4. Интегральный гиперболический косинус

;

5. Первый интеграл Френеля

;

6. Интеграл вероятности

;

7. Функция Бесселя первого рода , значение  также должно вводиться с клавиатуры

;

8. Функция Бесселя первого рода , значение  также должно вводиться с клавиатуры

;

9. Дилогарифм

.

Здесь  - постоянная Эйлера, - отношение длины окружности к ее диаметру.

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

  1.  Названия лабораторной работы.
  2.  Цели и содержания лабораторной работы.
  3.  Ответов на контрольные вопросы лабораторной работы.
  4.  Формулировки индивидуальных заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

  1.  Какой алгоритм является алгоритмом циклической структуры?
  2.  Типы циклов в языке Си.
  3.  Назовите основные параметры цикла.
  4.  Что представляют собой операторы инкремента и декремента, в каких формах они используются?
  5.  Как записывается условие продолжения цикла в циклах типа while  и  do ... while?
  6.  Основная форма цикла dowhile.
  7.  Какие три раздела записываются в круглых скобках для оператора цикла типа for? Какой разделитель между разделами?
  8.  Что такое вложенные циклы? Примеры.
  9.  Как образуется бесконечный цикл и  как выйти из него?
  10.   Схема приведения цикла while к эквивалентному ему циклу for.
  11.   Назовите операторы, способные изменять естественный порядок выполнения вычислений.
  12.   Для чего нужен оператор break?
  13.   Где употребляется оператор continue и для чего он используется?
  14.   Для чего используется оператор return?
  15.   Какой тип должно иметь выражение?

Пример выполнения лабораторной работы №3:

1. Индивидуальное задание №1:

1.1. Постановка задачи:

Составить UML-диаграмму деятельности и программу с использованием конструкций цикла для решения задачи.

Задача: в 1985 г. урожай ячменя составил 20 ц с га. В среднем каждые 2 года за счет применения передовых агротехнических приемов, урожай увеличился на 5%. Определить, через сколько лет урожай достигнет 25 ц с га.

1.2. UML-диаграмма:

1.3. Листинг программы:

// Лабораторная работа №3

// Индивидуальное задание №1

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

const double productivity = 20, prirost =  productivity/100*2.5;

int count = 0;

 cout<< "Лабораторная работа № 3\n";

 cout<< "\nГорошко А.А., БАС-051\n";

 cout<< "\nВариант № 6\n";

 

cout<< "\n\nИндивидуальное задание № 1:\n";

 cout<< "\nрешить задачу с использованием конструкций цикла.\n";

 cout<< "\n\nЗадача:\n";

cout<< "\nв 1985 г. урожай ячменя составил 20 ц с га. В среднем каждые 2 года за\n";

cout<< "\nсчет применения передовых агротехнических приемов урожай увеличился на 5%.\n";

cout<< "\nОпределить, через сколько лет урожай достигнет 25 ц с га.\n";

 cout<< "\n\nРабота программы:\n";

 

 while((prirost*count + 20) < 25)

{

 count++;

}

cout<< "\n\nУрожай достигнет 25 ц с га через: " <<count;

// выбор формы согласования (год, года или лет)

 while(count - 10 > 0)

{

 count = count - 10;

}

 

switch (count)

{

 case 1:

  cout<< " год";

  break;

 case 2:

 case 3:

 case 4:

  cout<<" года";

  break;

 

 default:

  cout<<" лет";

  break;

}

 

getch();

 

return 0;

}

1.4. Результаты работы программы:

2. Индивидуальное задание №2:

2.1. Постановка задачи:

Составить UML-диаграмму деятельности и программу для нахождения значения конечной суммы:  Вводится целое число .

2.2. UML-диаграмма:

2.3. Листинг программы:

// Лабораторная работа №3

// Индивидуальное задание №2

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

double a, S=0;

const double x=1;

int i, N;

cout<<"Лабораторная работа №3\n";

 cout<<"\nГорошко А.А., БАС-051\n";

 cout<<"\nВариант №6\n";

 

cout<<"\n\nИндивидуальное задание №2:\n";

cout<<"\nнайти значение конечной суммы S при x=1.\n";

cout<<"\n\nРабота программы:\n";

 

cout<<"\nВведите значение N>0\n"<<"\nN=";

 cin>> N;

for(i=1; i<=N; i++)

{

 a=(exp(x/i)+exp(-x/i))/(i+2)*(i+2)*(i+2)*(i+2);

 S+=a;

}

cout<<"\nКонечная сумма:\n"<<"\nS=";

cout<< S;

 getch();

return 0;

}

2.4. Результаты работы программы:

3. Табуляция функции: 

3.1. Постановка задачи:

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

3.2. UML-диаграмма:

3.3. Листинг программы:

// Лабораторная работа №3

// Табуляция функции

// Лабораторная работа №3

// Табуляция функции

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

const double x_min=-15;

const double x_max=20;

const int N=1000;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

double y, dx=(x_max-x_min / N);

cout<<"Лабораторная работа №3\n";

 cout<<"\nГорошко А.А., БАС-051\n";

 cout<<"\nВариант №6\n";

 

cout<<"\n\nЗадание:\n";

 cout<<"\nпротабулировать функцию y в диапазоне от x_min=-15 до x_max=20\n";

 cout<<"\nв N=1000 равноудаленных точках.\n";

 cout<<"\n\nРабота программы:\n";

 

for (double x=x_min; x<=x_max; x+=dx)

{

 if (x>3.5) y=(log(x)*cos(2*x));

 else

  if (x=3.5) y=(pow(x*x*x-1,1/3));

  else y=(1+sin(x)*sin(x)-2*cos(2*x)*cos(2*x));

 cout<< "x=";

 cout<< x;

 cout<< "\ny=";

 cout<< y;

}

getch();

return 0;

}

3.4. Результаты работы программы:

4. Индивидуальное задание №3:

4.1. Постановка задачи:

Составить программу и произвести вычисление значения специальной функции второго интеграла Френеля:  по ее разложению в ряд с точностью , аргумент функции  вводится с клавиатуры.

4.2. UML-диаграмма:

4.3. Листинг программы:

// Лабораторная работа №3

// Индивидуальное задание №3

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

#include <tchar.h>

using namespace std;

double fact(double number)

{

int k, i;

i = 1; k = 1;

if (number == 0 || number == 1)

 return 1;

else

 while (i <= number)

 {

  k *= i;

  i++;

 }

return k;

}

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

double x, S, eps = 1e-10, a, n, pi;

char buff[256];

 pi = 3,14159265358;

 

 cout <<"Лабораторная работа № 3\n";

 cout <<"\nГорошко А.А., БАС-051\n";

 cout <<"\nВариант № 6\n";

 

cout <<"\n\nИндивидуальное задание № 3:\n";

cout <<"\nвычислить значение специальной функции второго интеграла Френеля\n";

cout <<"\nпо ее разложению в ряд с точностью eps=10^-10.\n";

cout <<"\n\nРабота программы:\n";

 

 cout <<"\nВведите значение x\n"<< "\nx = ";

 cin >> x;

a = 0;

S = x;

 

for(n = 1; fabs(a) < eps; n++)

  {

     a = pow(-1,n)*pow((pi/2),2*n+1)*pow(x,(4*n+3)) / (fact(2*n+1),(4*n+3));

     S += a ;

  }

 

cout << "\nS(" << x <<") = " << S << endl;

 

 getch();

return 0;

}

4.4. Результаты работы программы:

Лабораторная работа №4.
Одномерные массивы в языке C++

Цель работы и содержание: закрепление знаний об одномерных массивах, составление программ с одномерными массивами.

Ход работы

Основные сведения о массивах в языке C++. В случае использования  простых переменных каждой области памяти для хранения одной величины соответствует свое имя. Если требуется работать с группой величин одного типа, их располагают в памяти последовательно и дают им общее имя, а различают по порядковому номеру. Такая последовательность однотипных величин называется массивом. Массивы, как и любые другие объекты, можно размещать либо с помощью операторов описания в сегментах данных или стека, либо в динамической области памяти с помощью операций выделения памяти.

При описании массива после имени в квадратных скобках задается количество его элементов (размерность), например int a[10]. Массив располагается в зависимости от места его описания либо в сегменте данных, либо в сегменте стека, и все инструкции по выделению памяти формирует компилятор до выполнения программы. Вследствие этого размерность массива может быть задана только константой или константным выражением. Если при описании массива не указана размерность, должен присутствовать инициализатор, в этом случае компилятор выделит намять по количеству инициализирующих значений. В дальнейшем мы увидим, что размерность может быть опущена также в списке формальных параметров.

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

int а[10] = {1, 1, 2, 2, 5, 100};

Для данного массива элементы имеют номера от 0 до 9. Номер элемента указывается после его имени в квадратных скобках, например, а[0], а[3]. Инициализирующие значения для массивов записываются в фигурных скобках. Значения элементам присваиваются по порядку. Если элементов в массиве больше, чем инициализаторов, элементы, для которых значения не указаны, обнуляются:

int b[5] = {3.  2,  1};        //b[0]=3,b[l]=2,b[2]=l,b[3]=0,b[4]=0

Для доступа к элементу массива после его имени указывается номер элемента (индекс) в квадратных скобках. В следующем примере подсчитывается сумма элементов массива.

#include <iostream.h>

int main()

{

const int n = 10; int i, sum;

int marks[n] = {3. 4, 5, 4, 4};

setlocale( LC_ALL, "Russian" );

 for (i =0. sum = 0; i<n; i++) sum += marks[i];

cout << "Сумма элементов:  " << sum;

return 0;

}

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

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

Идентификатор массива является константным указателем на его нулевой элемент. Например, для массива из предыдущего листинга имя mark - это то же самое, что & mark[0], а к i-му элементу массива можно обратиться, используя выражение *(mark+i). Можно описать указатель, присвоить ему адрес начала массива и работать с массивом через указатель. Следующий фрагмент программы копирует все элементы массива а в массив b:

int а[100], b[100];

int *ра = а;                 // или int *p = &а[0];

int *pb = b;

for(int i = 0;  i<100;  1++)

*pb++ = *pa++;           // или pb[i] = pa[i];

Пример 4.1. Написать программу, которая для целочисленного массива из 100 элементов определяет, сколько положительных элементов располагается между его максимальным и минимальным элементами.

Запишем алгоритм в самом общем виде.

  1.  Определить, где в массиве расположены его максимальный и минимальный элементы, то есть найти их индексы.
  2.  Просмотреть все элементы, расположенные между ними. Если элемент массива больше нуля, увеличить счетчик элементов на единицу.

Ясно, что порядок расположения элементов в массиве заранее не известен, и сначала может следовать как максимальный, так и минимальный элемент, кроме того, они могут и совпадать. Поэтому прежде чем просматривать массив в поисках количества положительных элементов, требуется определить, какой из этих индексов больше. Запишем уточненный алгоритм:

1. Определить, где в массиве расположены его максимальный и минимальный элементы:

  •  задать начальные значения для индексов максимального и минимального элементов (например, равные нулю, но можно использовать любые другие
    значения индекса, не выходящие за границу массива).
  •  просмотреть массив, поочередно сравнивая каждый его элемент с ранее най денными максимумом и минимумом. Если очередной элемент больше ранее
    найденного максимума, принять этот элемент за новый максимум (т.е. запомнить его индекс). Если очередной элемент меньше ранее найденного
    минимума, принять этот элемент за новый минимум.

2. Определить границы просмотра массива для поиска положительных элементов, находящихся между его максимальным и минимальным элементами:

  •  если максимум расположен в массиве раньше, чем минимум, принять левую границу просмотра равной индексу максимума, иначе - индексу минимума.
  •  если максимум расположен в массиве раньше, чем минимум, принять правую границу просмотра равной индексу минимума, иначе - индексу максимума.
  1.  Обнулить счетчик положительных элементов. Просмотреть массив в указанном диапазоне. Если очередной элемент больше нуля, увеличить счетчик на единицу.

Для экономии времени при отладке значения элементов массива задаются путем инициализации.

Листинг 4.1

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

const int n = 10;

int a[n] = {1, 3, -5, 1, -2, 1, -1, 3, 8, 4};

int i, imax, imin, count;

 setlocale( LC_ALL, "Russian" );

 

 for(i = imax = imin = 0; i < n; i++)

{

 if(a[i] > a[imax]) imax = i;

 if(a[i] < a[imin]) imin = i;

}

cout << “\n\t max = “ << a[imax] << “\t min = “

 << a[imin] << endl;

int ibeg = (imax < imin) ? imax : imin;

int iend = (imax < imin) ? imin : imax;

cout << “\n\t ibeg = “ << ibeg << “\t iend = “

 << iend << endl;

for(count = 0, i = ibeg + 1; i < iend; i++)

 if(a[i] > 0) count++;

cout <<“Количество положительных “<< count <<endl;

 

getch();

 return 0;

}

В программе использована управляющая последовательность \t, которая задает отступ при выводе на следующую позицию табуляции. Массив просматривается, начиная с элемента, следующего за максимальным (минимальным), до элемента, предшествующего минимальному (максимальному). Индексы границ просмотра хранятся в переменных ibeg и iend. В приведенной выше программе для определения их значений используется тернарная условная операция. Можно поступить и по-другому: просматривать массив всегда от максимума к минимуму, а индекс при просмотре увеличивать или уменьшать в зависимости от их взаимного расположения.

В приведенной ниже программе направление просмотра, то есть приращение индекса, хранится в переменной d. Если массив просматривается «слева направо», она равна 1, иначе - -1. Обратите внимание и на изменившееся условие продолжения этого цикла.

Листинг 4.2

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

const int n = 10;

int a[n] = {1, 3, -5, 1, -2, 1, -1, 3, 8, 4};

int i, imax, imin, count;

 setlocale( LC_ALL, "Russian" );

for(i = imax = imin = 0; i < n; i++)

{

 if(a[i] > a[imax]) imax = i;

 if(a[i] < a[imin]) imin = i;

}

cout << “\n\t max = “ << a[imax] << “\t min = “

 << a[imin] << endl;

int d = 0;

if(imax < imin) d = 1;

else if(imax > imin) d = -1;

for(count = 0, i = imax + d; i != imin; i += d)

 if(a[i] > 0) count++;

cout << “Количество положительных “ << count

 << endl;

getch();

 return 0;

}

Ввод массива в этом варианте программы осуществляется с клавиатуры. Напоминаем, что в этом случае желательно для проверки вывести введенные значения на печать.

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

  1.  a[imin] расположен левее a[imax];
  2.  a[imin] расположен правее a[imax];
  3.  a[imin] и a[imax] совпадают.

Последняя ситуация имеет место, когда в массиве все элементы имеют одно и то же значение. Кстати, во втором варианте программы третий случай корректно обрабатывается благодаря значению d = 0 для этой ситуации. Желательно также проверить, как работает программа, если a[imin] и a[imax] расположены рядом, а также в начале и в конце массива (граничные случаи). Элементы массива нужно задавать как положительные, так и отрицательные.

Разница между приведенными способами решения несущественна, но первый вариант более «прозрачен», поэтому он, на наш взгляд, предпочтительнее.

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

Часто бывает, что точное количество элементов в исходном массиве не задано, но известно, что оно не может превышать некое конкретное значение. В этом случае память под массив выделяется «по максимуму», а затем заполняется только часть этой памяти. Память можно выделить либо с помощью оператора описания в стеке или сегменте данных, либо в динамической области. Фактическое количество введенных элементов запоминается в переменной, которая затем участвует в организации циклов по массиву, задавая его верхнюю границу. Этот подход является весьма распространенным, поэтому мы приводим ниже небольшую, но полезную программу, в которой выполняется только считывание элементов массива с клавиатуры и их вывод на экран:

const int n = 1000;

int a[n];

int i, kol_a;

setlocale( LC_ALL, "Russian" );

cout << “Введите количество элементов: “;

cin >> kol_a;

if(kol_a > n)

{

 cout << “Превышение размера массива“ << endl;

 return 1;

}

for(int i = 0; i < kol_a; i++) cin >> a[i];

for(int i = 0; i < kol_a; i++) cout << a[i] << “ “;

cout << endl;

Несмотря на то, что значение константы n определяется «с запасом», надо обязательно проверять, не запрашивается ли большее количество элементов, чем возможно. Привычка к проверке подобных, казалось бы, маловероятных случаев позволит вам создавать более надежные программы, а нет ничего более важного для программы, чем надежность.

Пример 4.2. Выполнить сортировку массива по возрастанию, состоящего из n элементов методом «пузырька».

На языке C++ пузырьковая сортировка массива может быть запрограммирована следующим образом:

Листинг 4.3

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

const int N = 1000;

int a[N];

int n, i, j, temp;

 setlocale( LC_ALL, "Russian" );

cout << “Введите количество элементов: “;

 cin >> n;

 if(n <= 0 || n > N)

 {

 cout << “Неверный размер массива“ << endl;

 return 1;

}

for(i = 0; i < n; i++) cin >> a[i];

for(i = 0; i < n; i++) cout << a[i];

cout << endl;

// Сортировка

for(i = 0; i < n; i++)

 for(j = i + 1; j < n; j++)

  if(a[i] > a[j])

  {

   temp = a[i]; a[i] = a[j];

   a[j] = temp;

  }

 cout << “После сортировки: “;

 for(i = 0; i < n; i++) cout << a[i] << “ “;

cout << endl;

 

getch();

 return 0;

}

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

  1.  Проработать примеры, приведенные в лабораторной работе.
  2.  Составить программу с использованием одномерных массивов для решения задачи. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №1. Вариант:

  1.  Ввести массив А из 10 элементов, найти наибольший элемент и  переставить его с первым элементом. Преобразованный массив вывести.
  2.  Ввести массив А из 10 элементов, найти произведение положительных элементов и вывести его на экран.
  3.  Ввести массив А из 10 элементов, найти наименьший элемент и  переставить  его с последним элементом. Преобразованный массив вывести.
  4.  Ввести массив А из 10 элементов, найти сумму отрицательных  элементов и вывести ее на экран.
  5.  Ввести массив А из  10  элементов, найти  сумму  элементов,  больших 3 и меньших 8 и вывести ее на экран.
  6.  Ввести массив А из  10  элементов, найти  разность положительных элементов,  и вывести ее на экран.
  7.  Ввести массив А из 10 элементов, найти произведение отрицательных  элементов и вывести его на экран.
  8.  В заданном массиве подсчитать число нулевых элементов и вывести на экран их индексы.
  9.  Составить программу, выдающую индексы заданного элемента или сообщающую, что такого элемента в массиве нет.
  10.  Ввести массив А из 10 элементов, найти произведение положительных элементов кратных 3, их количество и вывести результаты на экран.
  11.  Ввести массив А из 10 элементов, найти сумму отрицательных  элементов кратных 7, их количество и вывести результаты на экран.
  12.  Ввести массив А из  10  элементов, найти  сумму  элементов,  больших 2 и меньших 20 и кратных 8, их количество и вывести результаты на экран.
  13.  Ввести массив А из  10  элементов, найти  сумму  элементов,  меньших по модулю 3 и кратных 9, их количество и вывести результаты на экран.
  14.  Ввести массив А из 10  элементов, найти  разность положительных элементов кратных 11, их количество и вывести результаты на экран.
  15.  Ввести массив А из 10  элементов, найти  произведение элементов,  больших 8 и меньших 18 и кратных 10, их количество и вывести результаты на экран.
  16.  Ввести массив А из 10 элементов, найти  сумму  элементов кратных 2, их количество и вывести результаты на экран.
  17.  Ввести массив А из 10 элементов, найти квадраты элементов кратных 4 и их количество. Преобразованный массив вывести.
  18.  Ввести массив А из 10 элементов, найти сумму положительных элементов кратных 5, их количество и вывести результаты на экран.
  19.  Ввести массив А из 10 элементов. Определить количество элементов, кратных 3 и индексы последнего такого элемента.
  20.  В массивах U[7],  D[7],  V[7] содержатся значения утренней, дневной и вечерней температуры соответственно за каждый день недели. Подсчитать среднее значение дневной температуры за каждый день.
  21.  В массивах А[n], G[n], F[n] содержатся оценки учащихся по алгебре, геометрии и физике соответственно. Определить, по какому предмету лучше успеваемость.
  22.  В массивах U[7],  D[7],  V[7] содержатся значения утренней, дневной и вечерней температуры соответственно за каждый день недели. Сформировать массив S[7], в котором будут содержаться значения среднедневной температуры. Определить среднее значение температуры а неделю.
  23.  В массивах А[n], G[n], F[n] содержатся оценки учащихся по алгебре, геометрии и физике соответственно. Определить среднюю оценку по алгебре и количество учащихся, не имеющих ни одной «двойки».
  24.  Определить средний рост девочек и мальчиков одного класса. В классе учится n учеников. (n > 15).
  25.  В ЭВМ по очереди поступают результаты соревнований по плаванию на дистанции 200 м, в которых участвует n спортсменов (n > 10). Выдать на экран дисплея лучший результат.
  26.  Из массива целых чисел составить три других, в первый из которых записать числа, кратные 5, во второй - числа, кратные 7, а в третий - остальные числа.
  27.  Для заданного массива определить, каких элементов больше: положительных или отрицательных. Вывести на экран их количество.
  28.  В заданном одномерном массиве все отрицательные элементы заменить нулями и подсчитать их количество.

3. Составить программу с использованием одномерных массивов для решения задачи на переупорядочивание элементов массива. В качестве алгоритма сортировки использовать сортировку методом «пузырька». Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №2. Вариант:

1. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  сумму отрицательных элементов массива;
  2.  произведение элементов массива, расположенных между максимальным и минимальным элементами.

Упорядочить элементы массива по возрастанию.

2. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  сумму положительных элементов массива;
  2.  произведение элементов массива, расположенных между максимальным по модулю и минимальным по модулю элементами.

Упорядочить элементы массива по убыванию.

3. В одномерном массиве, состоящем из n целых элементов, вычислить:

  1.  произведение элементов массива с четными номерами;

сумму элементов массива, расположенных между первым и последним нулевыми элементами.

Преобразовать массив таким образом, чтобы сначала располагались все положительные элементы, а потом - все отрицательные (элементы, равные 0, считать положительными).

4. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  сумму элементов массива с нечетными номерами;
  2.  сумму элементов массива, расположенных между первым и последним отрицательными элементами.

Сжать массив, удалив из него все элементы, модуль которых не превышает 1. Освободившиеся в конце массива элементы заполнить нулями.

5. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

1) максимальный элемент массива;

2) сумму элементов массива, расположенных до последнего положительного элемента.

Сжать массив, удалив из него все элементы, модуль которых находится в интервале [а, b]. Освободившиеся в конце массива элементы заполнить нулями.

6. В одномерном массиве, состоящем из n целых элементов, вычислить:

  1.  номер максимального элемента массива;
  2.  произведение элементов массива, расположенных между первым и вторым нулевыми элементами.

Преобразовать массив таким образом, чтобы в первой его половине располагались элементы, стоявшие в нечетных позициях, а во второй половине - элементы, стоявшие в четных позициях.

7. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  номер минимального элемента массива;
  2.  сумму элементов массива, расположенных между первым и вторым отрицательными элементами.

Преобразовать массив таким образом, чтобы сначала располагались все элементы, модуль которых не превышает 1, а потом - все остальные.

8. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  максимальный по модулю элемент массива;
  2.  сумму элементов массива, расположенных между первым и вторым положительными элементами.

Преобразовать массив таким образом, чтобы элементы, равные нулю, располагались после всех остальных.

9. В одномерном массиве, состоящем из n целых элементов, вычислить:

1) минимальный по модулю элемент массива;

2) сумму модулей элементов массива, расположенных после первого элемента, равного нулю.

Преобразовать массив таким образом, чтобы в первой его половине располагались элементы, стоявшие в четных позициях, а во второй половине - элементы, стоявшие в нечетных позициях.

10. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  номер минимального по модулю элемента массива;

сумму модулей элементов массива, расположенных после первого отрицательного элемента.

Сжать массив, удалив из него все элементы, величина которых находится в интервале [а, b]. Освободившиеся в конце массива элементы заполнить нулями.

11. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  номер максимального по модулю элемента массива;
  2.  сумму элементов массива, расположенных после первого положительного элемента.

Преобразовать массив таким образом, чтобы сначала располагались все элементы, целая часть которых лежит в интервале [а, b], а потом - все остальные.

12. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  количество элементов массива, лежащих в диапазоне от А до В;
  2.  сумму элементов массива, расположенных после максимального элемента.

Упорядочить элементы массива по убыванию модулей элементов.

13. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  количество элементов массива, равных 0;
  2.  сумму элементов массива, расположенных после минимального элемента.

Упорядочить элементы массива по возрастанию модулей элементов.

14. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  количество элементов массива, больших С;
  2.  произведение элементов массива, расположенных после максимального по модулю элемента.

Преобразовать массив таким образом, чтобы сначала располагались все отрицательные элементы, а потом - все положительные (элементы, равные 0, считать положительными).

15. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  количество отрицательных элементов массива;

сумму модулей элементов массива, расположенных после минимального по модулю элемента.

Заменить все отрицательные элементы массива их квадратами и упорядочить элементы массива по возрастанию.

16. В одномерном массиве, состоящем из n целых элементов, вычислить:

  1.  количество положительных элементов массива;
  2.  сумму элементов массива, расположенных после последнего элемента, равного нулю.

Преобразовать массив таким образом, чтобы сначала располагались все элементы, целая часть которых не превышает 1, а потом - все остальные.

17. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  количество элементов массива, меньших С;

сумму целых частей элементов массива, расположенных после последнего отрицательного элемента.

Преобразовать массив таким образом, чтобы сначала располагались все элементы, отличающиеся от максимального не более чем на 20%, а потом - все остальные.

18. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  произведение отрицательных элементов массива;
  2.  сумму положительных элементов массива, расположенных до максимального элемента.

Изменить порядок следования элементов в массиве на обратный.

19. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  произведение положительных элементов массива;
  2.  сумму элементов массива, расположенных до минимального элемента.

Упорядочить по возрастанию отдельно элементы, стоящие на четных местах, и элементы, стоящие на нечетных местах.

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

1. Названия лабораторной работы.

2. Цели и содержания лабораторной работы.

3. Ответов на контрольные вопросы лабораторной работы.

4. Формулировки индивидуальных заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

  1.  Как объявляются одномерные массивы в языке C++?
    1.  Какими должны быть размерности при описании статического массива в языке C++?
      1.  Каков диапазон изменения индекса массива в языке C++?
        1.  Каким образом производится инициализация массива в языке C++?
        2.  Чем является идентификатор массива?
        3.  Для чего используется управляющая последовательность \t?

Пример выполнения лабораторной работы №4:

        1. Индивидуальное задание №1:

1.1. Постановка задачи:

Составить программу с использованием одномерных массивов для решения задачи.

Задача: ввести массив А из  10  элементов, найти  сумму  элементов,  меньших по модулю 5, и вывести ее на экран.

1.2. UML-диаграмма:

1.3. Листинг программы:

// Лабораторная работа №4

// Индивидуальное задание №1

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "windows.h"

#include "math.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

const int n = 10;

int marks [n] = {};

int a, i, sum = 0;

cout<<"Лабораторная работа № 4\n";

 cout<<"\nГорошко А.А., БАС-051\n";

 cout<<"\nВариант № 6\n";

 

cout<<"\n\nИндивидуальное задание № 1:\n";

cout<<"\nввести массив А из 10 элементов, найти сумму\n";

cout<<"\nэлементов, меньших по модулю 5, и вывести ее на экран.\n";

cout<<"\n\nРабота программы:\n";

 

cout<<"\nВведите элементы массива: \n\n";

 for(int i = 0; i < n; i++)

{

 cout<<"A["<<i<<"]=";

 cin >> marks[i];

 if(marks[i]< 0)

  marks[i] = marks[i] * -1;

 if(5 > marks[i])

  sum += marks[i];

}

 

cout<<"\nСумма = "<< sum;

 

getch();

 

return 0;

}

2.4. Результаты работы программы:

2. Индивидуальное задание №2:

2.1. Постановка задачи:

Составить программу с использованием одномерных массивов для решения задачи на переупорядочивание элементов массива. В качестве алгоритма сортировки использовать сортировку методом «пузырька».

Задача: в одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  минимальный элемент массива;

сумму элементов массива, расположенных между первым и последним положительными элементами.

Преобразовать массив таким образом, чтобы сначала располагались все элементы, равные нулю, а потом – все остальные.

2.2. UML-диаграмма:

2.3. Листинг программы:

// Лабораторная работа №4

// Индивидуальное задание №2

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

const int N = 1000;

int a[N];

int n, i, j, temp;

int firstPos = -1, lastPos;

cout<<"Лабораторная работа № 4\n";

cout<<"\nГорошко А.А., БАС-051\n";

cout<<"\nВариант № 6\n";

 

cout<<"\n\nИндивидуальное задание № 2:\n";

cout<<"\nв одномерном массиве, состоящем из n вещественных элементов, вычислить:\n";

cout<<"\n1) минимальный элемент массива;\n";

cout<<"\n2) сумму элементов массива, расположенных между первым\n";

cout<<"\nи последним положительными элементами.\n";

cout<<"\nПреобразовать массив таким образом, чтобы сначала располагались\n";

cout<<"\nвсе элементы, равные нулю, а потом – все остальные.\n";

cout<<"\n\nРабота программы:\n";

 

cout<<"\nВведите количество элементов: ";

 cin >> n;

 if(n <= 0 || n > N)

 {

 cout<<"Неверный размер массива"<< "\n";

 return 1;

}

 cout<<"\nВведите элементы массива: \n\n";

 

for(i = 0; i < n; i++)

 cin >> a[i];

 

// Поиск минимального элемента массива

 temp = a[0];

 for(i = 0; i < n; i++)

{

 if(a[i] < temp)

  temp = a[i];

 }

cout<<"\nМинимальный элемент массива: "<< temp << "\n";

 

// Сумма элементов между первым и последним положительными элементами

 temp = 0;     

   for(i = 0; i < n; i++)

   {

        // первый положительный элемент не найден и текущий >0?

        if(firstPos == -1 && a[i] > 0)     

              firstPos = i;     // тогда записываем координаты текущего элемента как первого положительного

        if(a[i] > 0)

              lastPos = i;

   }

   if(firstPos != -1)

   {

 for(i = firstPos; i <= lastPos; i++)

 {

  temp += a[i];

 }

}

 

cout<<"\nСумма элементов массива, расположенных между";

 cout<<"\nпервым и последним положительными элементами: "<< temp << "\n";

 

 // Сортировка

temp = 0;

for(i = 0; i < n; i++)

 for(j = 0; j < n; j++)

 if(a[j+1] == 0)

 {

  temp = a[j];

a[j] = a[j+1];

  a[j+1] = temp;

 }

 cout<<"\nПосле сортировки: ";

 for(i = 0; i < n; i++) cout << a[i] << " ";

 cout << endl;

getch();

return 0;

}

2.4. Результаты работы программы:


Лабораторная работа №5.
Указатели и ссылки  в языке C++

Цель работы и содержание: закрепление знаний об указателях и ссылках, составление программ с указателями и ссылками.

Ход работы

Основные сведения об указателях в языке C++.  Когда компилятор обрабатывает оператор определения переменной, например, int i=10;, он выделяет память в соответствии с типом (int) и инициализирует ее указанным значением (10). Все обращения в программе к переменной по ее имени (i) заменяются компилятором на адрес области памяти, в которой хранится значение переменной. Программист может определить собственные переменные для хранения адресов областей памяти. Такие переменные называются указателями.

Указатели предназначены для хранения адресов областей памяти. В C++ различают три вида указателей - указатели на объект, на функцию и на void, отличающиеся свойствами и набором допустимых операций. Указатель не является самостоятельным типом, он всегда связан с каким-либо другим конкретным типом.

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

тип (*имя) ( список_типов_аргументов );

Например, объявление:

int (*fun) (double, double);

задает указатель с именем fun на функцию, возвращающую значение типа int и имеющую два аргумента типа double.

Указатель на объект содержит адрес области памяти, в которой хранятся данные определенного типа (основного или составного). Простейшее объявление указателя на объект (в дальнейшем называемого просто указателем) имеет вид:

тип *имя;

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

Звездочка относится непосредственно к имени, поэтому для того, чтобы объявить несколько указателей, требуется ставить ее перед именем каждого из них. Например, в операторе

int *а, b, *с;

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

Размер указателя зависит от модели памяти. Можно определить указатель на указатель и т.д.

Указатель на void применяется в тех случаях, когда конкретный тин объекта, адрес которого требуется хранить, не определен (например, если в одной и той же переменной в разные моменты времени требуется хранить адреса объектов различных типов).

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

Указатель может быть константой или переменной, а также указывать на константу или переменную. Рассмотрим примеры:

int i; //  целая переменная

const int ci = 1; //  целая константа

int * pi;         //  указатель на целую переменную

const int * pci:  //  указатель на целую константу

int * const cp = &i; //  указатель-константа на

                    //  целую переменную

const int * const cpc = &ci: //  указатель

                  //  константа на целую константу

Как видно из примеров, модификатор const, находящийся между именем указателя и звездочкой, относится к самому указателю и запрещает его изменение, a const слева от звездочки задает постоянство значения, на которое он указывает. Для инициализации указателей использована операция получения адреса &.

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

Инициализация указателей. Указатели чаще всего используют при работе с динамической памятью, называемой некоторыми эстетами кучей (перевод с английского языка слова heap). Это свободная память, в которой можно во время выполнения программы выделять место в соответствии с потребностями. Доступ к выделенным участкам динамической памяти, называемым динамическими переменными, производится только через указатели. Время жизни динамических переменных - от точки создания до конца программы или до явного освобождения памяти. В C++ используется два способа работы с динамической памятью. Первый использует семейство функций mallос и достался в наследство от С, второй использует операции new и delete.

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

Существуют следующие способы инициализации указателя:

1. Присваивание указателю адреса существующего объекта:

1.1. с помощью операции получения адреса:

int a = 5; // целая переменная

int* р = &а; //в указатель записывается адрес а

int* p (&а); // то же самое другим способом

1.2. с помощью значения другого инициализированного указателя:

int* r = p;

 

1.3. с помощью имени массива или функции, которые трактуются как адрес:

int b[10];  // массив

int* t = b;  // присваивание адреса начала массива

void f(int a ){ /* ... */ } // определение функции

void (*pf)(int);  // указатель на функцию

pf = f;  // присваивание адреса функции

2. Присваивание указателю адреса области памяти в явном виде:

char* vp = (char *)0xB8000000;

Здесь 0хВ8000000 - шестнадцатеричная константа, (char *) - операция приведения типа: константа преобразуется к типу «указатель на char».

3.Присваивание пустого значения:

int* suxx = NULL;

int* rulez = 0;

В первой строке используется константа NULL, определенная в некоторых заголовочных файлах С как указатель, равный нулю. Можно использовать просто 0, так как это значение типа int будет правильно преобразовано стандартными способами в соответствии с контекстом. Поскольку гарантируется, что объектов с нулевым адресом нет, пустой указатель можно использовать для проверки, ссылается указатель на конкретный объект или нет.

4. Выделение участка динамической памяти и присваивание ее адреса указателю: 

4.1. с помощью операции new:

int* n = new int;                      // 1

int* m = new int (10);                 // 2

int* q = new int [10];                 // 3

4.2. с помощью функции malloc:

int* u = (int *)malloc(sizeof(int));   // 4

В операторе 1 операция new выполняет выделение достаточного для размещения величины типа int участка динамической памяти и записывает адрес начала этого участка в переменную n. Память под саму переменную n (размера, достаточного для размещения указателя) выделяется на этапе компиляции.

В операторе 2, кроме описанных выше действий, производится инициализация выделенной динамической памяти значением 10.

В операторе 3 операция new выполняет выделение памяти под 10 величин типа int (массива из 10 элементов) и записывает адрес начала этого участка в переменную q, которая может трактоваться как имя массива. Через имя можно обращаться к любому элементу массива. Если память выделить не удалось, по стандарту должно порождаться исключение bad_alloc. Старые версии компиляторов могут возвращать 0.

В операторе 4 делается то же самое, что и в операторе 1, но с помощью функции выделения памяти malloc, унаследованной из библиотеки С. В функцию передается один параметр - количество выделяемой памяти в байтах. Конструкция (int*) используется для приведения типа указателя, возвращаемого функцией, к требуемому типу. Если память выделить не удалось, функция возвращает 0.

Операцию new использовать предпочтительнее, чем функцию malloc, особенно при работе с объектами.

Освобождение памяти, выделенной с помощью операции new, должно выполняться с помощью delete, а памяти, выделенной функцией mallос - посредством функции free. При этом переменная-указатель сохраняется и может инициализироваться повторно. Приведенные выше динамические переменные уничтожаются следующим образом:

delete n;

delete m;

delete [] q;

free (u);

Если память выделялась с помощью new[], для освобождения памяти необходимо применять delete[]. Размерность массива при этом не указывается. Если квадратных скобок нет, то никакого сообщения об ошибке не выдается, но помечен как свободный будет только первый элемент массива, а остальные окажутся недоступны для дальнейших операций. Такие ячейки памяти называются мусором.

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

int *(*р[10])();

объявляется массив из 10 указателей на функции без параметров, возвращающих указатели на int.

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

При интерпретации сложных описаний необходимо придерживаться правила «изнутри наружу»:

1)  если справа от имени имеются квадратные скобки, это массив, если скобки круглые - это функция;

2)  если слева есть звездочка, это указатель на проинтерпретированную ранее конструкцию;

3)  если справа встречается закрывающая круглая скобка, необходимо применить приведенные выше правила внутри скобок, а затем переходить наружу;

4)  в последнюю очередь интерпретируется спецификатор типа.

Для приведенного выше описания порядок интерпретации указан цифрами:

  

 int *(*р[10])();

  5  4 2   1 3   // порядок интерпретации описания

Операции с указателями. С указателями можно выполнять следующие операции: разадресация, или косвенное обращение к объекту (*), присваивание, сложение с константой, вычитание, инкремент (++), декремент (--), сравнение, приведение типов. При работе с указателями часто используется операция получения адреса (&).

Операция разадресации, или разыменования, предназначена для доступа к величине, адрес которой хранится в указателе. Эту операцию можно использовать как для получения, так и для изменения значения величины (если она не объявлена как константа):

char a;                     // переменная типа char

char * р = new char;    /* выделение памяти под указатель и под динамическую переменную типа char */

*р = 'Ю'; а = *р;       /* присваивание значения обеим переменным*/

Как видно из примера, конструкцию *имя_указателя можно использовать в левой части оператора присваивания, так как она является L-значением, то есть определяет адрес области памяти. Для простоты эту конструкцию можно считать именем переменной, на которую ссылается указатель. С ней допустимы все действия, определенные для величин соответствующего типа (если указатель инициализирован). На одну и ту же область памяти может ссылаться несколько указателей различного типа. Примененная к ним сиерация разадресации даст разные результаты. Например, программа

#include <stdio.h>

int main()

{

unsigned long int A = 0Xcc77ffaa;

unsigned short int* pint = (unsigned short int*) &A;

unsigned char* pchar = (unsigned char *) &A;

printf(“ | %x | %x | %x |”,  A, *pint, *pchar);

return 0;

}

на IBM PC-совместимом компьютере выведет па экран строку:

| cc77ffaa | ffaa | аа |

Значения указателей pint и pchar одинаковы, но разадресация pchar дает в результате один младший байт по этому адресу, a pint - два младших байта. В приведенном выше примере при инициализации указателей были использованы операции приведения типов. Синтаксис операции явного приведения типа прост: перед именем переменной в скобках указывается тип, к которому ее требуется преобразовать. При этом не гарантируется сохранение информации, поэтому в общем случае явных преобразований типа следует избегать.

При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*. Указатель может неявно преобразовываться в значение типа bool (например, в выражении условного оператора), при этом ненулевой указатель преобразуется в true, а нулевой в false. Присваивание без явного приведения типов допускается в двух случаях:

  1.  указателям типа void*;
  2.  если тип указателей справа и слева от операции присваивания один и тот же.

Таким образом, неявное преобразование выполняется только к типу void*. Значение 0 неявно преобразуется к указателю на любой тип. Присваивание указателей на объекты указателям на функции (и наоборот) недопустимо. Запрещено и присваивать значения указателям-константам, впрочем, как и константам любого типа (присваивать значения указателям на константу и переменным, на которые ссылается указатель-константа, допускается).

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

Инкремент перемещает указатель к следующему элементу массива, декремент - к предыдущему. Фактически значение указателя изменяется на величину sizeof (тип). Если указатель на определенный тип увеличивается или уменьшается на константу, его значение изменяется на величину этой константы, умноженную на размер объекта данного типа, например:

short * р = new short [S3;

р++;   // значение р увеличивается на 2

long * q = new long [5];

q++;    // значение q увеличивается на 4

Разность двух указателей - это разность их значений, деленная на размер типа в байтах (в применении к массивам разность указателей, например, на третий и шестой элементы равна 3). Суммирование двух указателей не допускается.

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

*р++ = 10;

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

*р = 10; р++;

Выражение (*р)++, напротив, инкрементирует значение, на которое ссылается указатель.

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

Динамические массивы. Если до начала работы программы неизвестно, сколько в массиве элементов, в программе следует использовать динамические массивы. Память под них выделяется с помощью операции new или функции mallос в динамической области памяти во время выполнения программы. Адрес начала массива хранится в переменной-указателе.

Обращение к элементу динамического массива осуществляется так же, как и к элементу обычного - например а[3]. Можно обратиться к элементу массива и другим способом - *(а+3). В этом случае мы явно задаем те же действия, что выполняются при обращении к элементу массива обычным образом. Рассмотрим их подробнее. В переменной-указателе а хранится адрес начала массива. Для получения адреса третьего элемента к этому адресу прибавляется смещение 3. Операция сложения с константой для указателей учитывает размер адресуемых элементов, то есть на самом деле индекс умножается на длину элемента массива: а + 3*sizeof(int). Затем с помощью операции * (разадресации) выполняется выборка значения из указанной области памяти.

Если динамический массив в какой-то момент работы программы перестает быть нужным и мы собираемся впоследствии использовать эту память повторно, необходимо освободить ее с помощью операции delete[], например:

delete[] a;

Размерность массива при этом не указывается.

Таким образом, время жизни динамического массива, как и любой динамической переменной, - с момента выделения памяти до момента ее освобождения. Область действия зависит от места описания указателя, через который производится работа с массивом. Область действия и время жизни указателей подчиняются общим правилам, рассмотренным на первом семинаре. Как вы помните, локальная переменная при выходе из блока, в котором она описана, «теряется». Если эта переменная является указателем и в ней хранится адрес выделенной динамической памяти, при выходе из блока эта память перестает быть доступной, однако не помечается как свободная, поэтому не может быть использована в дальнейшем. Это называется утечкой памяти и является распространенной ошибкой:

{ // пример утечки памяти

 int n;

cin >> n;

int* pmas = new int[n];

 

}

// После выхода из блока указатель pmas недоступен

Пример 5.1. Написать программу, которая для вещественного массива из n элементов определяет сумму его элементов, расположенных правее последнего отрицательного элемента.

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

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

Листинг 5.1

#include "conio.h"

#include "math.h"

#include "windows.h"

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

int n;

setlocale( LC_ALL, "Russian" );

 

 cout<<"\nВведите количество элементов: ";

 

int i, ineg;

 float sum, *a = new float[n]; // 1

cout<< "\nВведите элементы массива: \n\n";

 for(i = 0; i < n; i++) cin >> a[i];

for(i = 0; i < n; i++) cout << a[i] << ‘ ‘; // 2

for(i = 0; i < n; i++)

 if(a[i] < 0) ineg = i; // 3

for(sum = 0., i = ineg + 1; i < n; i++)

 sum += a[i]; // 4

cout<<"\nСумма: "<< sum;

delete[] a;

 return 0;

}

Поскольку количество элементов заранее не задано, память под массив выделяется в операторе 1 на этапе выполнения программы с помощью операции new. Выделяется столько памяти, сколько необходимо для хранения n элементов вещественного типа, и адрес начала этого участка заносится в указатель а.

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

С целью оптимизации программы может возникнуть мысль объединить цикл нахождения этого номера с циклами ввода и контрольного вывода элементов массива, но это не рекомендуется делать, потому что ввод данных, их вывод и анализ - разные по смыслу действия, и смешивание их в одном цикле не прибавит программе ясности. После отладки программы контрольный вывод (оператор 2) можно удалить или закомментировать.

Теперь перейдем к критическому анализу нашей первой попытки решения задачи. Для массивов, содержащих отрицательные элементы, эта программа работает верно, но при их отсутствии, как правило, завершается аварийно. Это связано с тем, что если в массиве нет ни одного отрицательного элемента, переменной ineg значение не присваивается. Поэтому в операторе for (оператор 4) будет использовано значение ineg, инициализированное произвольным образом. Поэтому в программу необходимо внести проверку, есть ли в массиве хотя бы один отрицательный элемент. Для этого переменной ineg присваивается начальное значение, не входящее в множество допустимых индексов массива (например, -1). После цикла поиска номера отрицательного элемента выполняется проверка, сохранилось ли начальное значение ineg неизменным. Если это так, это означает, что условие a[i]<0 в операторе 3 не выполнилось ни разу, и отрицательных элементов в массиве нет:

Листинг 5.2

#include "stdafx.h"

#include "conio.h"

#include "math.h"

#include "windows.h"

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

 setlocale( LC_ALL, "Russian" );

int n;

 

 cout<<"\nВведите количество элементов: ";

 cin >> n;

 

 int i, ineg = -1;

float sum, *a = new float[n];               // 1

 

cout<<"\nВведите элементы массива: \n\n";

 for(i = 0; i < n; i++) cin >> a[i];

 

for(i = 0; i < n; i++) cout << a[i] << ‘ ‘; // 2

 

for(i = 0; i < n; i++)

 if(a[i] < 0) ineg = i;                 // 3

 

if(ineg != -1)

{

 for(sum = 0., i = ineg + 1; i < n; i++)

  sum += a[i];                      // 4

cout<<"\nСумма: "<< sum;

 }

 delete[] a;

 

getch();

 return 0;

}

Можно предложить и более рациональное решение этой задачи: просматривать массив в обратном порядке, суммируя его элементы, и завершить цикл, как только встретится отрицательный элемент:

 

 Листинг 5.3

#include "stdafx.h"

#include "conio.h"

#include "math.h"

#include "windows.h"

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

int n;

setlocale( LC_ALL, "Russian" );

 cout<<"\nВведите количество элементов: ";

 cin >> n;

 

 int i, ineg = -1;

float sum, *a = new float[n];         // 1

cout<<"\nВведите элементы массива: \n\n";

 for(i = 0; i < n; i++) cin >> a[i];

 

bool flag_neg = false;

float sum = 0.f;

for(i = n - 1; i >= 0; i--)

{

 if(a[i] < 0)

 {

  flag_neg = true;

  break;

 }

 sum += a[i];

}

 

if(flag_neg)

cout<<"\nСумма: "<< sum;

else 

 cout<<"\nОтрицательных элементов нет ";

 

 getch();

 return 0;

}

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

Для исчерпывающего тестирования этой программы необходимо ввести по крайней мере три варианта исходных данных - когда массив содержит один, несколько или ни одного отрицательного элемента.

Пример 5.2. Написать программу, которая упорядочивает вещественный массив методом быстрой сортировки.

Идея алгоритма состоит в следующем. Применим к массиву так называемую процедуру разделения относительно среднего элемента. Вообще-то, в качестве «среднего» можно выбрать любой элемент массива, но для наглядности мы будем выбирать по возможности, средний по своему номеру элемент.

Процедура разделения делит массив на две части. В левую помещаются элементы, меньшие, чем элемент, выбранный в качестве среднего, а в правой - большие. Это достигается путем просмотра массива попеременно с обоих концов, при этом каждый элемент сравнивается с выбранным средним, и элементы, находящиеся в «неподходящей» части, меняются местами. После завершения процедуры разделения средний элемент оказывается на своем окончательном месте. Далее процедуру разделения необходимо повторить отдельно для левой и правой части: в каждой части выбирается среднее, относительно которого она делится на две, и так далее.

Понятно, что одновременно процедура не может заниматься и левой, и правой частями, поэтому необходимо каким-то образом запомнить запрос на обработку одной из двух частей (например, правой), и заняться оставшейся частью (например, левой). Так продолжается до тех пор, пока не окажется, что очередная обрабатываемая часть содержит ровно один элемент. Тогда нужно вернуться к последнему из необработанных запросов, применить к нему все ту же процедуру разделения и так далее. В конце концов, массив окажется полностью упорядочен.

Для хранения границ еще не упорядоченных частей массива более всего подходит структура данных, называемая стеком. Стек является одной из наиболее часто употребляемых структур данных и функционирует по принципу: «первым пришел, последним ушел».

В приведенной ниже программе стек реализуется в виде двух массивов stackr и stackl и одной переменной sp, используемой как «указатель» на вершину стека (она хранит номер последнего заполненного элемента массива). Для этого алгоритма количество элементов в стеке не может превышать n, поэтому размер массивов задан равным именно этой величине. При занесении в стек переменная sp увеличивается на единицу, а при выборке - уменьшается.

Ниже приведена программа, реализующая этот алгоритм:

Листинг 5.4

#include “stdafx.h”

#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

const int n = 20;

float *arr = new float[n], middle, temp;

int *stackl = new int[n], *stackr = new int[n];

int sp = 0, i, j, left, right;

setlocale( LC_ALL, "Russian" );

 

 cout << “Введите элементы массива: “;

 

for(i = 0; i < n; i++) cin >> a[i];

 

// Сортировка

sp = 1; stackl[1] = 0; stackr[1] = n - 1; // 1

 

while(sp > 0)

 {                                         // 2

 // Выборка из стека последнего запроса

 left = stackl[sp];                    // 3

 right = stackr[sp];                   // 4

 sp--;                                 // 5

 

 while(left < right)

 {                                    // 6

 // Разделение {arr[left]..arr[right]}

  i = left; j = right;              // 7

  middle = arr[(left + right)/2];   // 8

  

  while(i < j)

  {                                 // 9

   while(arr[i] < middle) i++;   // 10

   while(middle < arr[j]) j--;   // 11

   

   if(i <= j)

   {

    temp = arr[i];

    arr[i] = arr[j];

    arr[j] = temp;

    i++; j--;

   }

  }

  

  if(i < right)

  {                                 // 12

   //Запись в стек запроса из правой части

   sp++;

   stackl[sp] = i;

   stackr[sp] = right;   

  }

  

  right = j;                        // 13

  // Теперь left и right ограничивают левую часть

 }

}

 

// Вывод результата

for(i = 0; i < n; i++) cout << arr[i] << “ “;

cout << endl;

 

// Удаление динамических массивов

 delete[] arr;

 delete[] stackl;

delete[] stackr;

 

getch();

return 0;

}

На каждом шаге сортируется один фрагмент массива. Левая граница фрагмента хранится в переменной left, правая - в переменной right. Сначала фрагмент устанавливается размером с массив целиком (строка 1). В операторе 8 выбирается «средний» элемент фрагмента.

Для продвижения по массиву слева направо в цикле 10 используется переменная i, справа налево - переменная j (в цикле 11). Их начальные значения устанавливаются в операторе 7. После того, как оба счетчика «сойдутся» где-то в средней части массива, происходит выход из цикла 9 на оператор 12, в котором заносятся в стек границы правой части фрагмента. В операторе 13 устанавливаются новые границы левой части для сортировки на следующем шаге.

Если сортируемый фрагмент уже настолько мал, что сортировать его не требуется, происходит выход из цикла 6, после чего выполняется выборка из стека границ еще не отсортированного фрагмента (операторы 3,4). Если стек пуст, происходит выход из главного цикла 2. Массив отсортирован.

Ссылки. Ссылка представляет собой синоним имени, указанного при инициализации ссылки. Ссылку можно рассматривать как указатель, который всегда разыменовывается. Формат объявления ссылки:

тип & имя;

где тип - это тип величины, на которую указывает ссылка, & - оператор ссылки, означающий, что следующее за ним имя является именем переменной ссылочного типа, например:

int kol;

int& pal = kol; //ссылка pal-альтернативное имя для kol

const char& CR = "\n";     // ссылка на константу 

Запомните следующие правила.

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

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

Ссылка, в отличие от указателя, не занимает дополнительного пространства в памяти и является просто другим именем величины. Операция над ссылкой приводит к изменению величины, на которую она ссылается.

Пример 5.3. Составить программу нахождения тех элементов массива С (из n элементов), индексы которых являются степенями двойки (1, 2, 4, 8…). UML-диаграмма этого алгоритма приведена на рисунке 5.1.

Рисунок 5.1 - UML-диаграмма деятельности для примера 5.3

Листинг 5.5

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

const int N = 1000;

int n, i, j, left, right, sp = 0, m = 1;

float *C = new float[N], middle, temp;

int *stackl = new int[N], *stackr = new int[N];

cout<<"\n\nЗадание:\n";

cout<<"\nСоставить программу с использованием динамических массивов\n";

cout<<"\nдля решения задачи на переупорядочивание элементов массива.\n";

cout<<"\nВ качестве алгоритма сортировки использовать метод быстрой сортировки массива.\n";

cout<<"\n\nЗадача: составить программу нахождения тех элементов массива С,\n";

cout<<"\nиндексы которых являются степенями двойки (1, 2, 4,...).\n";

cout<<"\n\nРабота программы:\n"

 

A:

 cout<<"\nВведите количество элементов: ";

 cin >> n;

 if(n <= 0 || n > N)

 {

 cout<<"Неверный размер массива"<< "\n";

 goto A;

}

 cout<<"\nВведите элементы массива: \n\n";

 for(i = 0; i < n; i++) cin >> C[i];

 

sp = 1;

stackl[1] = 0;

stackr[1] = n - 1;

 

while(sp > 0)

{

 left = stackl[sp];

 right = stackr[sp];

 sp--;

 while(left < right)

 {

  i = left;

  j = right;

  middle = C[(left + right)/2];

  while(i < j)

  {

   while(C[i] < middle) i++;

   while(middle < C[j]) j--;

   if(i <= j)

   {

    temp = C[i];

    C[i] = C[j];

    C[j] = temp;

    i++;

    j--;

   }

  }

  if(i < right)

  {

   sp++;

   stackl[sp] = i;

   stackr[sp] = right;

  }

  right = j;

 }

}

 cout<<"\nМассив после сортировки:\n\n";

for(i = 0; i < n; i++)

 {

 cout << C[i] << "\n";

}

  

 cout<<"\nМассив из элементов, индексы которых являются степенями двойки:\n\n"<<"\n" ;

 for(i = 0; i < n; i++)

{

 while (m < n)

 {

  cout << C[m] << "\n" ;

  m = m * 2;

 }

}

 

delete[] C;

delete[] stackl;

 delete[] stackr;

getch();

return 0;

}

Пример 5.4. В массиве {aj}, j = 1, 2, …10 есть положительные и отрицательные элементы. Вычислить произведение  отрицательных элементов. UML-диаграмма этого алгоритма приведена на рисунке 5.2.

Рисунок 5.2 - UML-диаграмма деятельности для примера 5.4

Листинг 5.6

// Лабораторная работа №_5_1

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "windows.h"

#include <tchar.h>

#include <math.h>

#include <time.h>

#include <iomanip>

using namespace std;

const int n = 1000;

int _tmain(int argc, _TCHAR* argv[])

{  

   int n;

   int RANGE_MIN = 0;

   int RANGE_MAX = 20;

setlocale( LC_ALL, "Russian" );

    cout<< "Лабораторная работа №_5_1" << '\n';

cout<< '\n';

cout<< " В массиве {aj}, j = 1, 2, ...10 есть положительные и отрицательные элементы. " ;

cout<< '\n';

cout<< " Вычислить произведение отрицательных элементов " ;

cout<< '\n'<< '\n'<< '\n';

cout<< "\nВведите резмерность массива " ;  

cin >> n;

int i = 0, s = 0, klav;

double  *arr = new double[n];

double  piece = 1;

  

cout<< '\n';

cout<< " Как будет проидведенно заполнение? " ;

cout<< '\n';

cout<<"  1 - случайным образом " ;

cout<< '\n';

cout<< "  2 - в ручную " ;

cout<< '\n';

cout<<"Для продолжения нажмите клавишу выбора    ";

cin >> klav;

cout << '\n';

if (klav == 1)

{  

srand( (unsigned int)time( NULL ) );

for( i = 0; i < n; i++)

{  

int rand100 = (((double) rand() / (double) RAND_MAX) * RANGE_MAX + RANGE_MIN);

  arr[i]=rand100-10;

}

for( i = 0; i < n; i++)

{

cout << arr[i] << "  ";

}

}

if (klav==2)

{

for( i = 0; i < n; i++)

{

  cout<< "Введите "  << i+1 ;

  cout<< " элемент ";

  cin >> arr[i];

}

 for( i = 0; i < n; i++)

{

  cout << arr[i] << "  ";

}

}

if  (klav != 1 && klav !=2 )

{

 cout<< "Неправильный выбор" ;

 cout<< " Для выходна нажмите любую клавишу...";

goto stop;

}

cout << '\n';

for( i = 0; i < n; i++)

{

if (arr[i] < 0)

{

piece = piece*arr[i];

s++;

}

}

if (s==0)

{

cout<< '\n '<<"В массиве отсутствуют отрицательные элементы";  

}

else      

{

cout<< '\n'  <<" Произведение отрицательных элементов = " << piece;

}

delete []arr;

stop: getch ();

return 0;

}

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание в соответствии с индивидуальным заданием лабораторной работы №4. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

  1.  Проработать примеры, приведенные в лабораторной работе.

         2.  Составить программу с использованием динамических массивов для решения задачи. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

        Индивидуальное задание №1. Вариант:

  1.  Сформировать массив, содержащий 7 элементов, задав элементы с клавиатуры. Определить количество элементов, кратных 3 и индексы последнего такого элемента.
    1.  В заданном массиве подсчитать число нулевых элементов и вывести а экран их индексы.
    2.  Сформировать с помощью датчика случайных чисел массив, элементы которого находятся в промежутке от -60 до 60. Подсчитать в нем количество положительных элементов.
    3.  В заданном одномерном массиве все отрицательные элементы заменить нулями и подсчитать их количество.
    4.  В массивах U[7],  D[7],  V[7] содержатся значения утренней, дневной и вечерней температуры соответственно за каждый день недели. Подсчитать среднее значение дневной температуры за каждый день.
    5.  В массивах А[n], G[n], F[n] содержатся оценки учащихся по алгебре, геометрии и физике соответственно. Определить, по какому предмету лучше успеваемость.
    6.  Сформировать с помощью датчика случайных чисел массив A[n], элементы которого находятся в промежутке от -60 до 60. Создать массив B[n], каждый элемент которого вычисляется по формуле:

  1.  В заданном одномерном массиве найти максимальный элемент и поменять его местами с последним элементом массива.
    1.  Задан массив из N случайных чисел, принадлежащих промежутку [-50, 100]. Найти сумму тех элементов массива, которые больше 15, но меньше 45, а также вычислить количество этих элементов.
    2.  В массивах U[7],  D[7],  V[7] содержатся значения утренней, дневной и вечерней температуры соответственно за каждый день недели. Сформировать массив S[7], в котором будут содержаться значения среднедневной температуры. Определить среднее значение температуры за неделю.
    3.  В массивах А[n], G[n], F[n] содержатся оценки учащихся по алгебре, геометрии и физике соответственно. Определить среднюю оценку по алгебре и количество учащихся, не имеющих ни одной «двойки».
    4.  Из массива целых чисел составить три других, в первый из которых записать числа, кратные 5, во второй - числа, кратные 7, а в третий - остальные числа.
    5.  Дана последовательность из 100 различных чисел. Составить программу нахождения суммы чисел этой последовательности, расположенных между максимальным и минимальным числами (в сумму включить и оба эти числа)
    6.  Задан одномерный числовой массив. Вычислить сумму произведений всех пар соседних чисел
    7.  Определить в одномерном числовом массиве число соседств из двух чисел разного знака.
    8.  Проверить, имеется ли в данном одномерном числовом массиве хотя бы одна пара чисел, совпадающих по величине.
    9.  Проверить, имеется ли в одномерном числовом массиве хотя бы одна пара соседних чисел, являющихся противоположными.
    10.  Дан числовой массив a1, a2, …an.  Вывести на печать массив b1, b2,… bn, в котором bi= a1, b2= a1+a2, b3= a1+a2+a3 и т.д.
    11.  В заданном массиве найти максимальный элемент. Элементы, стоящие после максимального элемента заменить нулями.
    12.  Из чисел a1, a2, …. , an  выбрать те, которые меньше заданного числа с , и образовать из них новый массив, сохранив порядок следования элементов.
    13.  В массиве {aj}, j = 1, 2, …10 есть хотя бы один отрицательный элемент. Вычислить произведение элементов массива до первого отрицательного.
    14.  В массиве {aj}, j = 1, 2, …8 есть хотя бы один нуль. Вычислить произведение массива до первого нуля
    15.  В массиве {aj}, j = 1, 2, …8 есть хотя бы один отрицательный элемент. Вычислить сумму элементов массива до первого отрицательного элемента.
    16.  В массиве {aj}, j = 1, 2, …8 есть хотя бы один ноль. Вычислить сумму элементов массива до первого нуля.
    17.  В массиве {aj}, j = 1, 2, …10 есть положительные и отрицательные элементы. Вычислить произведение  положительных элементов.
    18.  В массиве {aj}, j = 1, 2, …10 подсчитать количество элементов, больших 3
    19.  Найти сумму первых чисел последовательности a1, a2, …., an, произведение которых не превосходит заданного числа M.

28.     Из чисел  a1, a2, ..., an   выбрать те, которые больше по модулю заданного числа  c, и образовать из них новый массив, сохранив порядок следования элементов.

          3. Составить программу с использованием динамических массивов для решения задачи на переупорядочивание элементов массива. В качестве алгоритма сортировки использовать метод быстрой сортировки массива. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

        Индивидуальное задание №2. Вариант:

  1.  Массив содержит 2n чисел. Из суммы первых n его элементов вычесть сумму последних n элементов.
    1.  Даны два массива разных размеров. Определить, какие элементы первого массива и сколько раз встречаются во втором массиве.
    2.  Даны два целочисленных массива одинакового размера. Получить третий массив того же размера, каждый элемент которого равен большему из соответствующих элементов данных массивов.
    3.  Задан массив С, содержащий m  чисел. Составить программу формирования массивов А и В, включая в массив А четные по номеру элементы массива С в порядке их следования, а в массив В - нечетные.
    4.  Заданы два массива А и В, содержащие по n чисел. Составить программу формирования массива С, включая в него сначала все элементы массива А, затем все элементы массива В.
    5.  Числовой массив a1, a2, …an упорядочен по возрастанию. Известно, что число х принадлежит отрезку числовой оси, вмещающему заданный массив. Определить номер k, для которого ak-1< x <= ak.
    6.  Заменить отрицательные элементы в числовом массиве из n чисел (n>10) их квадратами, оставив остальные без изменения.
    7.  В заданном массиве найти среднее арифметическое положительных чисел, средне арифметическое отрицательных чисел и число нулей.
    8.  В массиве из 2n чисел найти сумму квадратов элементов с четными индексами и сумму кубов элементов с нечетными индексами.
    9.  Транспонировать массив, т.е. по а1, а2,..., аn сформировать аn, аn-1, ..., a1.
    10.  Из заданного целочисленного массива удалить все повторяющиеся элементы, оставив только их первые вхождения, т.е. из заданного массива получить новый массив, состоящий из различных целых чисел.
    11.  Заменить отрицательные числа в массиве их квадратами, оставив остальные без изменения.
    12.  В заданном массиве найти среднее арифметическое положительных чисел, среднее арифметическое отрицательных чисел и число нулей.
    13.  В массиве из 2n чисел найти сумму квадратов элементов с четными индексами и сумму кубов элементов с нечетными индексами.
    14.  Из чисел а1, а2,..., аn выбрать те, которые больше по модулю заданного числа с, и образовать из них новый массив, сохранив порядок следования элементов.
    15.  Из массива целых чисел составить три других, в первый из которых записать числа, кратные 5, во второй - числа, кратные 7, а в третий - остальные числа.
    16.  Задан массив из 100 целых случайных чисел, принадлежащих промежутку [0, 100]. Найти сумму тех элементов массива, которые больше 15, но меньше 45, а также вычислить количество этих элементов.
    17.  В линейном массиве заменить все элементы на число m (m - индекс максимального элемента).
    18.  Дан массив, состоящий как из положительных, так и отрицательных чисел. Нужно сначала записать положительные числа, а затем отрицательные в том же порядке, как они были расположены в исходном массиве. Если есть нули, записать их в последнюю очередь.
    19.  Найти сумму элементов данного массива. Разделить каждый элемент исходного массива на полученное значение.
    20.  Вычислить сумму и разность массивов одного размера.
    21.  Найти среднее арифметическое значение элементов заданного массива. Преобразовать исходный массив, вычитая из каждого элемента среднее значение.
    22.  Даны два массива одинакового размера. Рассматривая их как арифметические векторы, найти длины этих векторов и их скалярное произведение.
    23.  Заданы два массива разных размеров. Объединить их в один массив, включив второй массив между k-ым и (k + 1)-ым элементами первого (k задано).
    24.  Вычесть из положительных элементов данного массива элемент с номером k1 а к отрицательным элементам прибавить элемент с номером k2. Нулевые элементы заменить 1. Номера k1 и k2 вводятся с клавиатуры.
    25.  К четным элементам целочисленного массива прибавить данное число а, а из элементов с четными номерами вычесть данное число b.
    26.  Дан первый член геометрической прогрессии и ее знаменатель. Сформировать одномерный массив, элементами которого служат первые n членов этой прогрессии.
    27.  Вставить одно и то же число, введенное с клавиатуры, перед каждым отрицательным элементом заданного целочисленного массива.

29. Дан массив четного размера. Поменять местами его половины следующим образом: первый элемент - с последним, второй - с предпоследним элементом и т.д.

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

1. Названия лабораторной работы.

2. Цели и содержания лабораторной работы.

3. Ответов на контрольные вопросы лабораторной работы.

4. Формулировки индивидуальных заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

  1.  Что является указателем в языке C++?
  2.  Какие виды указателей существуют в языке C++?
  3.  Каким образом осуществляется инициализация указателя в языке C++?
  4.  Какие существуют способы инициализации указателя?
  5.  Время жизни динамических переменных.
  6.  С помощью какой языковой конструкции выделяется память под динамический массив в языке C++?
  7.  Какие операции можно выполнять с указателями?
  8.  Что является ссылкой в языке C++?
  9.   Формат объявления ссылки.
  10.   К чему приводит операция над ссылкой?

Пример выполнения лабораторной работы №5:

       1. Индивидуальное задание №1:

1.1. Постановка задачи:

Составить программу с использованием динамических массивов для решения задачи.

Задача: составить программу, выдающую индексы заданного элемента или сообщающую, что такого элемента в массиве нет.

1.2. UML-диаграмма:

1.3. Листинг программы:

// Лабораторная работа №5

// Индивидуальное задание №1

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

const int N = 1000;

int n, i, a;

float *C = new float[N];

bool flag = false;

 cout<<"Лабораторная работа № 5\n";

 cout<<"\nГорошко А.А., БАС-051\n";

 cout<<"\nВариант № 6\n";

 

cout<<"\n\nИндивидуальное задание № 1:\n";

cout<<"\nСоставить программу с использованием динамических\n";

cout<<"\nмассивов для решения задачи.\n";

cout<<"\n\nЗадача: составить программу, выдающую индексы заданного элемента\n";

cout<<"\nили сообщающую, что такого элемента в массиве нет.\n";

cout<<"\n\nРабота программы:\n";

 

A:

 cout<<"\nВведите количество элементов: ";

 cin >> n;

 if(n <= 0 || n > N)

 {

 cout<<"\nНеверный размер массива!"<< "\n";

 goto A;

}

 cout<<"\nВведите элементы массива: \n\n";

 for(i = 0; i < n; i++) cin >> C[i];

 if(false)

{

B:

 cout<<"\nТакого элемента в массиве нет!"<< "\n";  

}

 cout<<"\nВведите элемент массива, индексы которого хотите узнать: ";

 cin >> a;

 

for(i = 0; i < n; i++)

 {

 if (C[i] == a)

 {

  cout<<"\nИндекс заданного элемента: ";

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

  flag = true;

 }

 else if((i+1) == n && flag == false)

  goto B;

}

  

delete[] C;

getch();

 return 0;

}

1.4. Результаты работы программы:

2. Индивидуальное задание №2:

2.1. Постановка задачи:

Составить программу с использованием динамических массивов для решения задачи на переупорядочивание элементов массива. В качестве алгоритма сортировки использовать метод быстрой сортировки массива. В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  1.  максимальный по модулю элемент массива;
  2.  сумму элементов массива, расположенных между первым и вторым положительными элементами.

Преобразовать массив таким образом, чтобы элементы, равные нулю, располагались после всех остальных.

 2.2. UML-диаграмма:

2.3. Листинг программы:

#include "stdafx.h"

#include <iostream>

#include <conio.h>

#include <tchar.h>

using namespace std;

int sortirovka(float array[],int numberel)

{

int j=0;

float sum=0;

cout<<endl;

/********Алгоритм быстрой сортировки*******/

float middle,temp;

int *stackl=new int [numberel], *stackr=new int [numberel];

int sp,i,left,right;

sp=1;

stackl[1]=0; stackr[1]=numberel-1;

while(sp>0)

{

left=stackl[sp];

right=stackr[sp];

sp--;

while(left<right)

{

i=left;j=right;

middle=array[(left+right)/2];

while(i<j)

{

while(array[i]<middle) i++;

while(middle<array[j]) j--;

if (i<=j)

{

temp=array[i];

array[i]=array[j];

array[j]=temp;

i++;j--;

}

}

if (i<right)

{

sp++;

stackl[sp]=i;

stackr[sp]=right;

}

right=j;

}

}

cout <<"Результат быстрой сортировки: "<<endl;

for(i=0;i<numberel;i++) cout << array[i]<<" ";

cout<<endl;

delete [] stackl;

delete [] stackr;

/*******Сумма первых неотрицательных чисел******/

sum=0;

for(int i=0;i<numberel;++i)

{

if (array[i]>0)

{

while (array[i]>0 && i<numberel)

{

sum+=array[i];

++i;

}

break;

}

}

cout<<"Сумма первых неотрицательных чисел = "<<sum<<endl;

j=0;

cout<<endl;

for(int i=0;i<numberel;++i)

if (array[i]!=0) {array[j]=array[i]; ++j;}

for(int i=j;i<numberel;++i)

array[i]=0;

/*********Массив с нулями в конце************/

cout<<"Массив с нулями в конце: "<<endl;

cout<<endl;

for(int i=0;i<numberel;++i)

{

cout<<array[i]<<"\t";

}

cout<<endl;

delete [] array;

return 0;

}

int _tmain(int argc, _TCHAR* argv[])

{

int metod=0;

int n=13,max,j=0;

cout<<"Способ заполнения массива: "<<endl;

cout<<"\t1-Вручную\n";

cout<<"\t2-Случайное заполнение\n";

cin>>metod;

if (metod==1)

{

cout<<"Введите число элементов ";

cin>>n;

float *m=new float [n],sum;

cout<<endl;

for(int i=0;i<n;++i)

{

++j;

cout<<"Введите "<<j<<" элемент массива: ";

cin>>m[i];

}

sortirovka(m,n);

}

if (metod==2)

{

cout<<"генерация массива"<<endl;

float *l=new float [n];

for(j=0;j<n;++j)

l[j]=rand() %13;

cout<<"Полученный массив"<<endl;

for(j=0;j<n;++j)

cout<<l[j]<<"\t";

cout<<endl;

sortirovka(l,n);

}

return 0;

}

2.4. Результаты работы программы:

Лабораторная работа №6.
Двумерные массивы в языке C++

Цель работы и содержание: закрепление знаний о двумерных массивах, составление программ с двумерными массивами.

Ход работы

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

int a[3][5]; //Целочисл. матрица из 3 строк и 5 столбцов

Массив хранится по строкам в непрерывной области памяти:

а00   а01  а02   а03   а04      а10   а11  а12   а13   а14      а20   а21  а22   а23   а24

/- - - 0-я строка - - - / - - - 1-я строка - - - - - / - - - 2-я строка - - - /

Строки массива ничем не отделены одна от другой. В памяти сначала располагается одномерный массив а[0], представляющий собой нулевую строку массива а, затем - массив а[1], представляющий собой первую строку массива а, и т. д. Количество элементов в каждом из этих массивов равно длине строки, то есть количеству столбцов в матрице. При просмотре массива от начала в первую очередь изменяется правый индекс (номер столбца).

Для доступа к отдельному элементу массива применяется конструкция вида а [ i ] [ j ], где 1 (номер строки) и j (номер столбца) - выражения целочисленного типа. Каждый индекс может изменяться от 0 до значения соответствующей размерности, уменьшенной на единицу.

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

Можно обратиться к элементу массива и другими способами: *(*(а + i) + j) или *(a[i] + j). Они приведены для лучшего понимания механизма индексации, поскольку здесь в явном виде записаны те же действия, которые генерируются компилятором при обычном обращении к массиву. Рассмотрим их подробнее.

Допустим, требуется обратиться к элементу, расположенному на пересечении второй строки и третьего столбца - а[2][3]. Как и для одномерных массивов, имя массива а представляет собой константный указатель на начало массива. В данном случае это массив, состоящий из трех массивов. Сначала требуется обратиться ко второй строке массива, то есть одномерному массиву а [2]. Для этого надо прибавить к адресу начала массива смещение, равное номеру строки, и выполнить разадресацию: *( а + 2). При сложении указателя с константой учитывается длина адресуемого элемента, поэтому на самом деле прибавляется число 2, умноженное на длину элемента, то есть 2 * (5 * sizeof(int)), поскольку элементом является строка, состоящая из 5 элементов типа int.

Далее требуется обратиться к третьему элементу полученного массива. Для получения его адреса опять применяется сложение указателя с константой 3 (на самом деле прибавляется 3 * sizeof(int)), а затем применяется операция разыменования для получения значения элемента: *(*(а + 2) + 3).

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

int а[3][5] = {1, 2, 1, 3, 5, 2, 3, 4, 5, 1,  1, 3, 2, 6, 1};

определяет матрицу со следующими значениями элементов:

1   2   1   3   5

2   3   4   5   1

1   3   2   6   1

Если количество значений в фигурных скобках превышает количество элементов в массиве, при компиляции будет выдано сообщение об ошибке. Если значений меньше, оставшиеся элементы массива инициализируются значением по умолчанию (для основных типов это 0). Можно задавать начальные значения не для всех элементов массива. Для этого список значений констант для каждой строки заключается в дополнительные фигурные скобки. Вот, например, как заполнить единицами нулевой и первый столбцы приведенного выше массива:

int а[3][5] = {{1, 1}, {1, 1}, {1, 1}};

Остальные элементы массива обнуляются.

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

int а[][5] = {{1, 1, 7, 7, 7},  {1, 1, 0},  {1, 1, 2, 2, 2}};

Пример 6.1. Среднее арифметическое и количество положительных элементов

Написать программу, которая для целочисленной матрицы 10 X 20 определяет среднее арифметическое ее элементов и количество положительных элементов в каждой строке.

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

Рисунок 6.1 - UML-диаграмма деятельности для примера 6.1

Листинг 6.1

#include <iostream.h>

#include <iomanip.h>

void main()

{

const int nrow = 10, ncol = 20;

int a[nrow][ncol];

int i, j;

 setlocale( LC_ALL, "Russian" );

cout << “Введите элементы массива:” << endl;

 

for (i = 0; i < nrow; i++)

 for (j = 0; j < ncol; j++) cin >> a[i][j];

for (i = 0; i < nrow; i++)

{

for ( j= 0; j < ncol; j++) cout << setw(4) << a[i][j] << “  ”  cout << endl;

}

 

int n_pos_el;            

 float s = 0;

 

for (i = 0; i < nrow; i++)   

{

n_pos_el = 0;

 

for (j = 0; j < ncol; j++)   

{

 s += a[i][j];

 if (a[i][j] > 0) n_pos_el++;

   }  

 cout << “Строка:”<< i << “количество:” << n_pos_el << endl;

}

 

s /= nrow * ncol;

 cout << “Среднее арифметическое:” << s << endl;

}

Размерности массива заданы именованными константами, что позволяет легко их изменять. Для упрощения отладки рекомендуется задать небольшие значения этих величин или приготовить тестовый массив в текстовом файле и изменить программу так, чтобы она читала значения из файла.

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

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

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

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

Следует записывать операторы инициализации накапливаемых в цикле величин непосредственно перед циклом, в котором они вычисляются.

При написании вложенных циклов необходимо следить за отступами. Все операторы одного уровня вложенности должны начинаться в одной и той же колонке. Это облегчает чтение программы и, следовательно, поиск ошибок. По вопросу расстановки фигурных скобок существуют разные мнения специалистов. В настоящее время наиболее распространенными являются два стиля: стиль 1TBS (One True Bracing Style), которого придерживались основоположники языка С - Б. Керниган (Brian Kerni-ghan) и Д. Ритчи (Dennis Ritchie), и стиль Алмена (Eric Allman).

Динамические массивы. В динамической области памяти можно создавать двумерные массивы с помощью операции new или функции malloc. Остановимся на первом варианте, поскольку он более безопасен и прост в использовании.

При выделении памяти сразу под весь массив количество строк (самую левую размерность) можно задавать с помощью переменной или выражения, а количество столбцов должно быть константным выражением, то есть явно определено до выполнения программы. После слова new записывается тип создаваемого массива, а затем - его размерности в квадратных скобках (аналогично описанию «обычных», нединамических массивов), например:

int n;

const int m = 5;

cin >> n;

int (*a)[m] = new int [n][m];              //1                                       

int ** b = (int **) new int [n][m];        //2                                   

В этом фрагменте показано два способа создания динамического массива. В операторе 1 адрес начала выделенного с помощью new участка памяти присваивается переменной а, определенной как указатель на массив из m элементов типа int. Именно такой тип значения возвращает в данном случае операция new. Скобки необходимы, поскольку без них конструкция интерпретировалась бы как массив указателей. Всего выделяется n элементов.

В операторе 2 адрес начала выделенного участка памяти присваивается переменной b которая описана как «указатель на указатель на int, поэтому перед присваиванием требуется выполнить преобразование типа.

По стандарту в этом случае рекомендуется применять другую операцию преобразования типа, но старые компиляторы могут ее не поддерживать:

int ** b = reinterpret_cast <int-**> (new int [n][m]);

Обращение к элементам динамических массивов производится точно так же, как к элементам «обычных», с помощью конструкции вида a[i ][ j].

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

Более универсальный и безопасный способ выделения памяти под двумерный массив, когда обе его размерности задаются на этапе выполнения программы, приведен ниже:

int nrow, ncol;

setlocale( LC_ALL, "Russian" );

cout << “Введите количество строк и столбцов:”;

cin >> nrow >> ncol;

int **a = new int *[nrow];                     //1                                           

for(int i = 0; i < nrow; i++)                  //2                                                        

a[i] = new int [ncol];                     //3                                                     

В операторе 1 объявляется переменная типа «указатель на указатель на i nt» и выделяется память под массив указателей на строки массива (количество строк - nrow). В операторе 2 организуется цикл для выделения памяти под каждую строку массива. В операторе 3 каждому элементу массива указателей на строки присваивается адрес начала участка памяти, выделенного под строку двумерного массива. Каждая строка состоит из ncol элементов типа int (рисунок 6.2).

int **a                int *a[nrow]                       int a[nrow][ncol]     

                                                                                         

                                                                          nstb

Рисунок 6.2 - Схема динамической области памяти, выделяемой под массивы

Освобождение памяти из-под массива с любым количеством измерений выполняется с помощью операции delete [].

Пример 6.2. Номер столбца из положительных элементов

Написать программу, которая для прямоугольной целочисленной матрицы определяет номер самого левого столбца, содержащего только положительные элементы. Если такого столбца нет, вывести сообщение.

UML-диаграмма алгоритма приведена на рисунке 6.3. Для решения этой задачи матрицу необходимо просматривать по столбцам. При этом быстрее меняется первый индекс (номер строки). Сделать вывод о том, что какой-либо столбец содержит только положительные элементы, можно только после просмотра столбца целиком; зато если в процессе просмотра встретился отрицательный элемент, можно сразу переходить к следующему столбцу.

Рисунок 6.3 - UML-диаграмма деятельности для примера 6.2

Эта логика реализуется с помощью переменной-флага allposit, которая перед началом просмотра каждого столбца устанавливается в значение true, а при нахождении отрицательного элемента «опрокидывается» в false. Если все элементы столбца положительны, флаг не опрокинется и останется истинным, что будет являться признаком присутствия в матрице искомого столбца. Если столбец найден, просматривать матрицу дальше не имеет смысла, поэтому выполняется выход из цикла и вывод результата.

Листинг 6.2

#include <iostream.h>

#include <iomanip.h>

int main()

{

int nrow, ncol;

 setlocale( LC_ALL, "Russian" );

cout << “Введите количество строк и столбцов:”;

 cin >> nrow >> ncol; //ввод размерности массива                               

 

int i, j;

int **a = new int *[nrow]; /* выделение памяти под массив */

 

for(i = 0; i < nrow; i++) a[i] = new int[ncol];

cout << “Введите элементы массива:” << endl;

 

for(i = 0; i < nrow; i++)

 

for(j = 0; j < ncol; j++) cin >> a[i][j];         // ввод массива

 

for(i = 0; i < nrow; i++)

{

  for(j = 0; j < ncol; j++) cout << setw(4) <<  a[i][j] << “ ”;

  cout << endl;

}

int num = -1;

bool all_posit;

for (j = 0; j< ncol; j++) // просмотр по столбцам

{                                 

all_posit = true;

 

for (i = 0; i < nrow; i++)  /* анализ элементов столбца */                         

 if (a[i][j]<0)

{

all_posit = false;

break;

}

if (all_posit)

{

num = j;

break;

}

}

if (-1 == num ) cout << “ Столбцов нет ” << endl;

else cout << “ Номер столбца: ” << num << endl;

return 0;

}

В программе необходимо предусмотреть случай, когда ни один столбец не удовлетворяет условию. Для этого переменной nun, в которой будет храниться номер искомого столбца, присваивается начальное значение, не входящее в множество значений, допустимых для индекса, например -1. Перед выводом результата его значение анализируется. Если оно после просмотра матрицы сохранилось неизменным, то есть осталось равным -1, то столбцов, удовлетворяющих заданному условию, в матрице нет.

Можно обойтись без анализа переменной num, да и вообще без этой переменной, если вывести номер столбца сразу после его определения, после чего завершить программу. Этот вариант приведен ниже.

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

Листинг 6.3

#include <iostream.h>

#include <iomanip.h>

int main()

{

 setlocale( LC_ALL, "Russian" );

ifstream fin(“input.txt” , ios :: in | ios :: nocreate);

 

if (!fin)

{

 cout << “ Файл input.txt не найден.” << endl;

return 1;  

}

ofstream fout (“output.txt”);

 

if (!fout)

{

 cout << “ Невозможно открыть файл для записи. ” << endl;

return 1;  

}

int nrow, ncol;

 

fin >> nrow >> ncol;

 

int i, j;

int **a = new int *[nrow];

 

for(i = 0; i < nrow; i++) a[i] = new int[ncol];

 

for (i = 0; i < nrow; i++)

 for(j = 0; j < ncol; j++) fin >> a[i][j];

 

for (i = 0; i < nrow; i++)

{

  for(j = 0; j < ncol; j++) fout << setw(4) << a[i][j] << “ ”;

  fout << end l;

}

bool all_posit;

for (j = 0; j < ncol; j++)

{

 all_posit = true;

 

for (i = 0; i < nrow; i++)

  if (a[i][j] < 0)

{

all_posit = false; break;

}

 

if (all_posit)

{

  fout << “ Номер столбца: ” << j;

  cout << “ Работа завершена ” << endl;

  return 0;

 }

}

 

fout << “ Столбцов нет ”;

 cout << “ Работа завершена ” << endl;

return 0;

}

Ввод размерности массива и его элементов выполняется из файла input.txt, расположенного в том же каталоге, что и программа, а результаты выводятся в файл output. txt. В программе определены объект fin класса входных файловых потоков и объект fout класса выходных файловых потоков. Файловые потоки описаны в заголовочном файле <fstream.h>. Работа с этими объектами аналогична работе со стандартными объектами cin и cout.

Предполагается, что файл с именем 1 nput. txt находится в том же каталоге, что и текст программы, иначе следует указать полный путь, дублируя символ обратной косой черты, так как иначе он будет иметь специальное значение, например:

ifstream fin("c:\\prim\\cpp\\input.txt", ios::in | ios::nocreate);

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

Если программа завершается успешно, то на экран выводится сообщение «Работа завершена».

Входной файл input.txt можно создать в любом текстовом редакторе. Он должен существовать до первого запуска программы. На расположение и формат исходных данных в файле никаких ограничений не накладывается.

Пример 6.3. Упорядочивание строк матрицы

Написать программу, которая упорядочивает строки прямоугольной целочисленной матрицы по возрастанию сумм их элементов.

I. Исходные данные, результаты и промежуточные величины.

Исходные данные. Поскольку размерность матрицы неизвестна, придется использовать динамический массив элементов целого типа. Ограничимся типом

int.

Результаты. Результатом является та же матрица, но упорядоченная. Это значит, что не следует заводить для результата новую область памяти, а необходимо упорядочить матрицу in situ, то есть на том же месте.

Промежуточные величины. Кроме конечных результатов, в любой программе есть промежуточные, а также служебные переменные. Следует выбрать их тип и способ хранения.

Если требуется упорядочить матрицу по возрастанию сумм элементов ее строк, эти суммы надо вычислить и где-то хранить. Поскольку все они потребуются при упорядочивании, их надо записать в массив, количество элементов которого соответствует количеству строк матрицы, а i-й элемент содержит сумму элементов i-й строки. Количество строк заранее неизвестно, поэтому этот массив также должен быть динамическим. Сумма элементов строки может превысить диапазон значений, допустимых для отдельного элемента строки, поэтому для элемента этого массива надо выбрать тип 1ong.

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

Любой алгоритм можно первоначально разбить на этапы ввода исходных данных, вычислений и вывода результата.

Вычисление в данном случае состоит из двух шагов: формирование сумм элементов каждой строки и упорядочивание матрицы. Упорядочивание состоит в выборе наименьшего элемента и обмене с первым из рассматриваемых. Разветвленные алгоритмы и алгоритмы с циклами полезно представить в виде обобщенной блок-схемы.

III. Когда алгоритм полностью прояснился, можно переходить к написанию программы. Одновременно с этим продумываются и подготавливаются тестовые примеры. Рекомендуется придумать переменным понятные имена и сразу же при написании аккуратно форматировать текст программы, чтобы по положению оператора было видно, на каком уровне вложенности он находится. Функционально завершенные части алгоритма отделяются пустой строкой, комментарием или хотя бы комментарием.

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

Листинг 6.4

#include <fstream.h>

#include <iomanip.h>

int main()

{

 setlocale( LC_ALL, "Russian" );

ifstream fin(“input.txt” , ios :: in | ios :: nocreate);

if (!fin)

{

 cout << “ Файл input.txt не найден.” << endl; return 1;

}

int nrow, ncol;

fin >> nrow >> ncol;    // ввод размерности массива                             

int i, j;

int **a = new int *[nrow]; /* выделение памяти под массив */                         

for(i = 0; i < nrow; i++) a[i] = new int [ncol];

for (i = 0; i < nrow; i++)          // ввод массива

for (j = 0; j < ncol; j++) fin >> a[i][j];

long *sum = new long [nrow]  /* массив сумм элементов строк */

for (i = 0; i < nrow; i++)

{

sum[i] = 0;

for (j = 0; j < ncol; j++) sum[i] += a[i][j];

}

for (i = 0; i < nrow; i++)     // контрольный ввод

{                              

for (j = 0; j < ncol; j++) cout << setw(4) << a[i][j] << “ ”;

cout << “| ” << sum[i] << endl;

}

cout << endl;

long buf_sum;

int nmin, buf_a;

for (i = 0; i < nrow - 1; i++)   // упорядочивание

{                   

nmin = i;

for (j = i + 1; j < nrow; j++)

 if (sum[j] < sum[nmin]) nmin = j;

buf_sum = sum[i]; sum[i] = sum[nmin]; sum[nmin] = buf_sum;

for (j = 0; j < ncol; j++)

{

 buf_a = a[i][j]; a[i][j] = a[nmin][j]; a[nmin][j] = buf_a;

 }

}

 

for (i = 0; i < nrow; i++)  /* вывод упорядоченной матрицы */

{   

for (j = 0; j < ncol; j++) cout << setw(4) << a[i][j] << “ ”;

cout << endl;

}

return 0;

}

    

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

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

Для контроля вместе с исходным массивом рядом с каждой строкой выводится сумма её элементов, отделенная вертикальной чертой.

В качестве второго тестового примера рекомендуется ввести значения элементов массива, близкие к максимальным для типа int. Дополнительно следует проверить, правильно ли упорядочивается массив из одной и двух строк и столбцов, поскольку многие ошибки при написании циклов связаны с неверным указанием их граничных значений.

Рекомендации по порядку создания программы.

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

Написать программу. При написании программы рекомендуется:

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

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

Пример 6.4. Сглаживание заданной вещественной матрицы, работа с файлами

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

Построить результат сглаживания заданной вещественной матрицы размером 10 на 10. В сглаженной матрице найти сумму модулей элементов, расположенных выше главной диагонали. Ввод и вовод данных в программе осуществить с помощью файла. UML-диаграмма этого алгоритма приведена на рисунке 6.4.

Рисунок 6.4 - UML-диаграмма деятельности для примера 6.4

Листинг 6.5

#include "stdafx.h"

#include <iostream>

#include <conio.h>

#include <tchar.h>

#include <fstream>

#include <iomanip>

using namespace std;

int main()

{

const int nrow=10;

const int ncol=10;

ifstream fin("input.txt",ios::in);

if (!fin)

{

cout<<"Can't find input.txt"<<endl;

return 1;

}

ofstream fout("output.txt");

if (!fout)

{

cout<<"Write falure: check permission"<<endl;

return 1;

}

int k,l,k1,l1,counter,i,j;

double sum,sumdiag=0;

//заполнение массива из файла

double a[nrow][ncol];

cout << "Чтение данных из файла..." << endl;

for(i=0;i<nrow;i++)

for(j=0;j<ncol;j++) fin>>a[i][j];

double m[nrow][ncol];

cout<<"Обработка данных..."<<endl;

//процедура сортировки

counter=0;sum=0;l=0;l1=0;

for(i=0;i<nrow;i++)

for(j=0;j<ncol;j++)

{

k=i-1;

k1=i+1;

if (k<0) k++;

if (k1>ncol-1) k1--;

if ((j<=ncol-1)&&(j>=ncol-i)) sumdiag = sumdiag + a[i][j];

for(k;k<=k1;k++)

{

l=j-1;

l1=j+1;

if (l<0) l++;

if (l1>ncol-1) l1--;

for(l;l<=l1;l++)

if ((k==i)&&(l==j)) continue;

else

{

sum=sum+a[k][l];

counter++;

}

}

m[i][j]=(float) sum/counter;

sum=0;

counter=0;

}

 

//запись отсортированного массива в файл

Cout << "Запись результатов обработки в файл..." << endl;

for(i=0;i<nrow;i++)

{

for(j=0;j<ncol;j++)

fout<<setw(5)<<left<<setprecision(3)

<<m[i][j]<<" ";

fout<<endl;

}

Fout << endl;

Fout << "Сумма всех эл-ов ниже глав. диагонали:" << setw(5) << setprecision(9) << sumdiag << endl;

cout << "Обработка закончена!" << endl;

return 0;

}

Рисунок 6.5 – Файл для входных данных

Рисунок 6.6 – Файл для выходных данных

Рисунок 6.7 – Вывод программы на экран

Пример 6.5. Определение количества отрицательных элементов в тех строках данной целочисленной прямоугольной матрицы, которые содержат хотя бы один нулевой элемент

Дана целочисленная прямоугольная матрица. Определить количество отрицательных элементов в тех строках, которые содержат хотя бы один нулевой элемент. UML-диаграмма этого алгоритма приведена на рисунке 6.8.

Рисунок 6.8 - UML-диаграмма деятельности для примера 6.5

Листинг 6.6

#include "stdafx.h"

#include <conio.h>

#include <windows.h>

#include <math.h>

#include <iostream>

#include <time.h>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

srand((unsigned)time(NULL));

int m,n,h,c,i,j,ch;

 

cout << "Лабораторная работа № 6\n";

cout << "\nЗадание\nДана целочисленная прямоугольная матрица. Определить:\ количество отрицательных элементов в тех строках,\nкоторые содержат хотя бы один нулевой элемент.";

cout << "\n Введите размерность массива\n";

cin >> n;

cin >> m;

 

int **a = new int *[n];

       for(i = 0; i < n; i++) a[i] = new int [m];

cout << "\nВыберите способ ввода массива";

cout << "\n1. Ввести массив с клавиатуры.\n2. Заполнить массив случайным образом.\n";

cin>>ch;

if (ch==1)

{

cout << "\n Введите элементы массива\n"<<endl;

 for(i = 0; i < n; i++)

         for(j = 0; j < m; j++) cin >> a[i][j];

   for(i = 0; i < n; i++)

{

           for(j = 0; j < m; j++)

cout << a[i][j] << ' ';

    cout << endl;

}

}

if (ch == 2)

{

for(i = 0; i < n; i++)

for(j = 0; j < m; j++)  a[i][j]=rand()%20;

   for(i = 0; i < n; i++)

{

           for(j = 0; j < m; j++)

cout << a[i][j] << ' ';

    cout << endl;

}

}

h = 0;

for(i = 0; i < n; i++)

{

 for (j = 0; j < m; j++)

  if (a[i][j] < 0)

   h++;

for (j = 0; j < m; j++)

if (a[i][j] == 0)

{

   cout << "\nНомер строки\n" << i;

   cout << "\nЧисло отрицательных элементов\n" << h;

   h = 0;

}

else

    cout << "\nНет нулевых элементов\n";

}

getch();

return 0;

}

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

1.Проработать примеры, приведенные в лабораторной работе.

2. Составить программу с использованием двумерных локальных массивов для решения задачи. Размерности локальных массивов задавать именованными константами, значения элементов массива - в списке инициализации. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №1. Вариант:

1. Дана целочисленная прямоугольная матрица. Определить:

  1.  количество строк, не содержащих ни одного нулевого элемента;
  2.  максимальное из чисел, встречающихся в заданной матрице более одного раза.

2. Дана целочисленная прямоугольная матрица. Определить количество столбцов, не содержащих ни одного нулевого элемента.

Характеристикой строки целочисленной матрицы назовем сумму ее положительных четных элементов. Переставляя строки заданной матрицы, расположить их в соответствии с ростом характеристик.

3. Дана целочисленная прямоугольная матрица. Определить:

  1.  количество столбцов, содержащих хотя бы один нулевой элемент;

номер строки, в которой находится самая длинная серия одинаковых элементов.

4. Дана целочисленная квадратная матрица. Определить:

  1.  произведение элементов в тех строках, которые не содержат отрицательных элементов;
  2.  максимум среди сумм элементов диагоналей, параллельных главной диагонали матрицы.

5. Дана целочисленная квадратная матрица. Определить:

  1.  сумму элементов в тех столбцах, которые не содержат отрицательных элементов;
  2.  минимум среди сумм модулей элементов диагоналей, параллельных побочной диагонали матрицы.

6. Для заданной матрицы размером 8 на 8 найти такие k, что k -я строка матрицы совпадает с k-м столбцом.

Найти сумму элементов в тех строках, которые содержат хотя бы один отрицательный элемент.

7. Характеристикой столбца целочисленной матрицы назовем сумму модулей его отрицательных нечетных элементов. Переставляя столбцы заданной матрицы, расположить их в соответствий с ростом характеристик.

Найти сумму элементов в тех столбцах, которые содержат хотя бы один отрицательный элемент.

8. Соседями элемента Aj в матрице назовем элементы Аk с i - 1 <  k  < i+1, j-1 < 1 < j + 1, (k , 1) / (i, j). Операция сглаживания матрицы дает новую матрицу того же размера, каждый элемент которой получается как среднее арифметическое имеющихся соседей соответствующего элемента исходной матрицы. Построить результат сглаживания заданной вещественной матрицы размером 7 на 7. В сглаженной матрице найти сумму модулей элементов, расположенных ниже главной диагонали.

9. Элемент матрицы называется локальным минимумом, если он строго меньше всех имеющихся у него соседей. Подсчитать количество локальных минимумов заданной матрицы размером 10 на 10.
Найти сумму модулей элементов, расположенных выше главной диагонали.

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

11. Уплотнить заданную матрицу, удаляя из нее строки и столбцы, заполненные нулями. Найти номер первой из строк, содержащих хотя бы один положительный элемент.

12. Осуществить циклический сдвиг элементов прямоугольной матрицы на п элементов вправо или вниз (в зависимости от введенного режима), п может быть больше количества элементов в строке или столбце.

13. Осуществить циклический сдвиг элементов квадратной матрицы размерности М х N вправо на к элементов таким образом: элементы 1-й строки сдвигаются в последний столбец сверху вниз, из него - в последнюю строку справа налево, из нее - в первый столбец снизу вверх, из него - в первую строку; для остальных элементов - аналогично.

14. Дана целочисленная прямоугольная матрица. Определить номер первого из столбцов, содержащих хотя бы один нулевой элемент.

Характеристикой строки целочисленной матрицы назовем сумму ее отрицательных четных элементов. Переставляя строки заданной матрицы, расположить их в соответствии с убыванием характеристик.

15. Упорядочить строки целочисленной прямоугольной матрицы по возрастанию количества одинаковых элементов в каждой строке.

Найти номер первого из столбцов, не содержащих ни одного отрицательного элемента.

16. Путем перестановки элементов квадратной вещественной матрицы добиться того, чтобы ее максимальный элемент находился в левом верхнем углу, следующий по величине - в позиции (2,2), следующий по величине - в позиции (3,3) и т. д., заполнив таким образом всю главную диагональ. Найти номер первой из строк, не содержащих ни одного положительного элемента.

17. Дана целочисленная прямоугольная матрица. Определить:

  1.  количество строк, содержащих хотя бы один нулевой элемент;

номер столбца, в котором находится самая длинная серия одинаковых элементов.

18. Дана целочисленная квадратная матрица. Определить:

  1.  сумму элементов в тех строках, которые не содержат отрицательных элементов;
  2.  минимум среди сумм элементов диагоналей, параллельных главной диагонали матрицы.

19. Дана целочисленная прямоугольная матрица. Определить:

  1.  количество положительных элементов в тех строках, которые не содержат нулевых элементов;
  2.  номера строк и столбцов всех седловых точек матрицы.

Примечание. Матрица А имеет седловую точку АР если АР является минимальным элементом в i-й строке и максимальным в j-м столбце.

3. Составить программу с использованием двумерных динамических массивов для решения задачи согласно варианту индивидуального задания №1.

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

1. Названия лабораторной работы.

2. Цели и содержания лабораторной работы.

3. Ответов на контрольные вопросы лабораторной работы.

4. Формулировки индивидуальных заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

1. Как представляется в C++ двумерный массив?

2. Где и каким образом хранится двумерный массив?

3. В каких пределах изменяются индексы двумерного массива?

4. Какими способами можно описать двумерный массив?

5. Какие действия выполняются для каждой строки при вычислении количества положительных элементов?

6. В каком месте программы следует записывать операторы инициализации накапливаемых в цикле величин?

7. Каким образом в динамической области памяти можно создавать двумерные массивы?

8. Способы создания динамического массива.

9. Каким образом производится обращение к элементам динамических массивов?

Пример выполнения лабораторной работы №6:

       1. Индивидуальное задание №1:

1.1. Постановка задачи:

Составить программу с использованием двумерных локальных массивов для решения задачи. Размерности локальных массивов задавать именованными константами, значения элементов массива - в списке инициализации.

Задача: дана целочисленная прямоугольная матрица. Определить:

  1.  сумму элементов в тех строках, которые содержат хотя бы один отрицательный элемент;

номера строк и столбцов всех седловых точек матрицы.

Примечание. Матрица А имеет седловую точку As, если As является минимальным элементом в i-й строке и максимальным в j-м столбце.

1.2. UML-диаграмма:

1.3. Листинг программы:

// Лабораторная работа №6

// Индивидуальное задание №1

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

const int nrow = 3, ncol = 5;

int A[nrow][ncol];

int i, j, summ;

 

cout<<"Лабораторная работа № 6\n"

     <<"\nГорошко А.А., БАС-051\n"

    <<"\nВариант № 6\n"

 

<<"\n\nИндивидуальное задание № 1:\n"

  <<"\nСоставить программу с использованием двумерных локальных массивов для\n"

  <<"\nрешения задачи. Размерности локальных массивов задавать именованными\n"

  <<"\nконстантами, значения элементов массива - в списке инициализации.\n"

  <<"\n\nЗадача: дана целочисленная прямоугольная матрица. Определить:\n"

  <<"сумму элементов в тех строках, которые содержат хотя бы один\n"

  <<"\nотрицательный элемент;\n"

  <<"\n2) номера строк и столбцов всех седловых точек матрицы.\n"

  <<"\n\nПримечание: матрица А имеет седловую точку As, если Аs является\n"

  <<"\nминимальным элементом в i-й строке и максимальным в j-м столбце.\n"

  <<"\n\nРабота программы:\n"

 

<<"\nВведите элементы массива: \n\n";

 

 for (i = 0; i < nrow; i++)

{

 for (j = 0; j < ncol; j++)

 {

  cout << "A[" << i << "][" << j << "] = ";

  cin >> A[i][j];   

 }   

}

 

cout << "\n1) ";

for (i = 0; i < nrow; i++)

{

 bool flag = false;

 summ = 0;

 for (j = 0; j < ncol; j++)

 {

  summ += A[i][j];

  if(A[i][j] < 0)

   flag = true;

 }   

 if(flag == true)

 {

  cout<<"Сумма элементов строки "<< i;

  cout<<" с отрицательным элементом"<< " = " << summ << "\n\n";

 }

}

cout<<"\n2) ";

for (i = 0; i < nrow; i++)

 {

 int min, max;

 int tempValue; // временная переменная

 min = 0;

 tempValue = A[i][0];

 // поиск минимального в строке элемента

 for (j = 0; j < ncol; j++)

 {

  if(A[i][j] < tempValue)

  {

   min = j;   

   tempValue = A[i][j];

  }

 }

 // поиск максимального в столбце элемента

 max = 0;

 tempValue = A[0][min];

 for(j = 0; j < nrow; j++)

 {

  if(A[j][min] > tempValue)

  {

   max = j;

   tempValue = A[j][min];

  }

 }

 if(i == max)

 {

  cout<<"Седловая точка: "<< "As[" << max << "][" << min << "]";

 }

}

 

getch();

return 0;

}

1.4. Результаты работы программы:

2. Индивидуальное задание №2:

2.1. Постановка задачи:

Составить программу с использованием двумерных динамических массивов для решения задачи индивидуального задания №1.

2.2. UML-диаграмма:

2.3. Листинг программы:

// Лабораторная работа №6

// Индивидуальное задание №2

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

int nrow, ncol, i, j, summ;

 

cout<<"Лабораторная работа № 6\n";

 cout<<"\nГорошко А.А., БАС-051\n";

 cout<<"\nВариант № 6\n";

 

cout<<"\n\nИндивидуальное задание № 2:\n";

cout<<"\nСоставить программу с использованием двумерных динамических\n";

cout<<"\nмассивов для решения задачи индивидуального задания №1.\n";

cout<<"\n\nРабота программы:\n";

 

cout<<"\nВведите количество строк и столбцов:\n\n"<< "i = " ;

 cin >> nrow;

cout<< "j = ";

cin >> ncol;

 

int **A = new int *[nrow];

cout<<"\nВведите элементы массива: \n\n";

 for (i = 0; i < nrow; i++)

{

 A[i] = new int[ncol];

 for (j = 0; j < ncol; j++)

 {

  cout << "A[" << i << "][" << j << "] = ";

  cin >> A[i][j];   

 }   

}

cout<<"\n1) ";

for (i = 0; i < nrow; i++)

{

 bool flag = false;

 summ = 0;

 for (j = 0; j < ncol; j++)

 {

  summ += A[i][j];

  if(A[i][j] < 0)

   flag = true;

 }   

 if(flag == true)

 {

  cout<<"Сумма элементов строки "<< i;

  cout<<" с отрицательным элементом"<< " = " << summ << "\n\n";

 }

}

 

cout<<"\n2) ";

for (i = 0; i < nrow; i++)

 {

 int min, max;

 int tempValue; // временная переменная

 min = 0;

 tempValue = A[i][0];

 // поиск минимального в строке элемента

 for (j = 0; j < ncol; j++)

 {

  if(A[i][j] < tempValue)

  {

   min = j;   

   tempValue = A[i][j];

  }

 }

 // поиск максимального в столбце элемента

 max = 0;

 tempValue = A[0][min];

 for(j = 0; j < nrow; j++)

 {

  if(A[j][min] > tempValue)

  {

   max = j;

   tempValue = A[j][min];

  }

 }

 if(i == max)

 {

  cout<<"Седловая точка: "<< "As[" << max << "][" << min << "]";

 }

}

  

getch();

return 0;

}

2.4. Результаты работы программы:

Лабораторная работа №7.
Функции и перегрузка в языке C++

Цель работы и содержание: закрепление знаний о функциях и перегрузке, составление программ с функциями и перегрузкой.

Ход работы

Основные сведения о функциях в языке С++. Функция - это группа операторов, выполняющая законченное действие. К функции можно обратиться по имени, передать ей значения и получить из нее результат.

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

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

Формат простейшего заголовка (прототипа) функции:

тип имя ([список параметров]);

В квадратных скобках записано то, что может быть опущено. Например, заголовок функции main обычно имеет вид:

int mainO;

             

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

double sin (double);

            

Здесь записано, что функция имеет имя sin, вычисляет значение синуса типа double, и для этого нужно передать ей аргумент типа double. А вот заголовок функции memcpy, копирующей блок памяти длиной n байтов, начиная с адреса src, по адресу dest:

void *memcpy (void *dest,const void *src,size t n);

            

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

void *memcpy(void *, const void *. size t);

               

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

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

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

int sum(int a,int b)     /* функция находит сумму двух значений */

{
   return a + b;           // тело функции

}

В тексте программы может содержаться произвольное количество объявлений одной и той же функции и только одно определение (в этом функции не отличаются от других программных объектов). Тело функции представляет собой блок, заключенный в фигурные скобки. Для возврата результата, вычисленного в функции, служит оператор return. После него указывается выражение, результат вычисления которого и передается в точку вызова функции. Результат при необходимости преобразуется по общим правилам к типу, указанному в заголовке. Функция может иметь несколько операторов возврата, это определяется алгоритмом.

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

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

double  y,  x1 = 0.34, x2 = 2;

y = sin (x1);

cout << y << ‘   ‘ << sin (x2) << endl;

y = sin (x1 + 0.5) - sin(x1 - 0.5);

char *cite = “ Never say never ”;

char  b [100];

memcpy (b, cite, strlen(cite) + 1);

int summa, a = 2;

summa = sum(a, 4);

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

Пример 7.1. Передача в функцию параметров стандартных типов

Написать программу вывода таблицы значений функции Ch x (гиперболический косинус) для аргумента, изменяющегося в заданных пределах с заданным шагом. Значения функции вычислять с помощью разложения в ряд Тейлора с точностью r.

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

Разработка любой функции ведется в том же порядке, что и разработка программы в целом. Сначала определяется интерфейс функции, то есть какие значения подаются ей на вход и что должно получиться в результате. Затем продумываются структуры данных, в которых будут храниться эти и промежуточные значения; затем составляется алгоритм, программа и тестовые примеры.

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

double cosh(double x, double eps);

             

Для вычисления суммы ряда понадобятся две промежуточные переменные - для хранения очередного члена ряда и его номера. Эти переменные должны быть описаны внутри функции, поскольку вне ее они не нужны.

include <stdio.h>

#include <math.h>

double cosh(double x, double eps); //прототип ф-ции

int main()

{

double Xn, Xk, dX, Eps;

printf("Enter Xn, Xk, dX, eps \n");

scanf("%lf%lf%lf%lf.&Xn, &Xk, &dX, &eps);

printf (................................ \n");

printf(|       X       |           Y |\n");

printf( .............................\n");

for (double x = Xn; x <= Xk; x += dX)

printf(“|%9.2]f |%14.6g |\n”\ x. cosh(x. eps));

printf("...............................\n);

return 0;

}

double cosh(double x. double eps)

{

const int Maxlter - 500;      /* максимальное количество итераций */

double ch = 1. у = ch;         /* первый член ряда и нач. значение суммы */

for (Int n = 0; fabs(ch) > eps; n++)

{

ch *- x * x /((2 * n + 1)*(2 * n + 2));       // член ряда

у += ch;   // добавление члена ряда к сумме

if (n > Maxlter)

{

puts(“ Ряд расходится!\n");

return 0;

}

}

return у;

}

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

Если определение функции размещается после ее вызова, то перед функцией, в которой он выполняется, размещают прототип (заголовок). Обычно заголовки всех используемых в программе функций размещают в самом начале файла или в отдельном заголовочном файле. Заголовок нужен для того, чтобы компилятор мог проверить правильность вызова функции. Стандартные заголовочные файлы, которые мы подключаем к программам, содержат прототипы функций библиотеки именно с этой целью.

В этой программе для ввода-вывода мы применили не классы, а функции, унаследованные из библиотеки языка С, поскольку с их помощью форматированный вывод записывается более компактно. Спецификация формата  применяется для вывода вещественных чисел в широком диапазоне значений. Первое число модификатора (14) задает, как и для других спецификаций, ширину отводимого под число поля, а второе (6) - не точность, как в формате f, а количество значащих цифр. При этом число выводится либо в формате f, либо в формате е (с порядком) в зависимости от того, какой из них получится короче.

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

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

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

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

Листинг 7.1

# include <stdio.h>

# include <math.h>

Double cosh(double x, double eps, int  &err);

int main()

{

double Xn, Xk, dX, Eps, y;

int err;

printf("Enter Xn, Xk, dX, eps \n");

scanf("%lf%lf%lf%lf.&Xn, &Xk, &dX, &eps);

printf (................................ \n");

printf(|       X       |           Y |\n");

printf( .............................\n");

for (double x = Xn; x <= Xk; x += dX)  

{

y = cosh(x, eps, err);

if (err) printf(“|%9.2]|Рядрасходится!|\n”,);

  printf(“|%9.2]f     |%14.6g |\n”\ x, y);

}

printf("...............................\n);

return 0;

}

double cosh(double x, double eps, int err)

{

err = 0;

const int Maxlter = 500;

double ch - 1. у - ch;

for (int n - 0: fabs(ch) > eps; n++) (ch *- x * x /((2 * n + l)*(2 * n + 2));

у +- ch;

if (n > Maxlter);

{

err – 1;

return 0;

}

}

for(double x=Xn; x<-Xk; x+-dX)

{

у'cosh(x.eps. err);

if (err) prmtf("|X9.21f|Ряд расходится!|\пи.x);

else    printf(MH!9.21f |*14.6g |\n\ x. y);

}

printfC ................................ \n");

return 0;

double cosh(double x. double eps.int &err)

{

err - 0;

const int Maxlter - 500;

double ch = 1. у = ch;

for (int n = 0: fabs(ch) > eps; n++)

{

ch *= x * x /((2 * n + l)*(2 * n + 2));

у += ch;

if (n > Maxlter),

{

err – 1;

return 0;

}

}

return y;

}

          

Недостатком этого метода является увеличение количества параметров функции. Знак & перед параметром егг - это признак передачи параметра по ссылке. Такой способ позволяет передавать значения из функции в вызывающую программу.

Механизм передачи параметров в функцию весьма прост. Когда мы пишем в списке параметров функции выражение вида double х, это значит, что в функцию при ее вызове должно быть передано значение соответствующего аргумента. Для этого в стеке создается его копия, с которой и работает функция. Естественно, что изменение этой копии не может оказать никакого влияния на ячейку памяти, в которой хранится сам параметр. Кстати, именно поэтому на месте такого параметра можно при вызове задавать и выражение, например:

у = cosh(x + 0.2. eps / 100. err );

            

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

Ссылка, синтаксически являясь синонимом имени некоторого объекта, в то же время содержит его адрес. Поэтому ссылку, в отличие от указателя, не требуется разадресовывать для получения значения объекта. Если мы передаем в функцию ссылку, то есть пишем в списке параметров выражение вида double Seps, а при вызове подставляем на его место аргумент, например eps fact, мы тем самым передаем в функцию адрес переменной eps fact. Этот адрес обрабатывается так же, как и остальные параметры: в стеке создается его копия. Функция, работая с копией адреса, имеет доступ к ячейке памяти, в которой хранится значение переменной eps fact, и тем самым может его изменить.

Можно передать в функцию и указатель; в этом случае придется применять операции разадресации и взятия адреса явным образом. Для нашей функции применение указателя для передачи третьего параметра будет выглядеть так:

// прототип функции;

double cosh(double х, double eps, int * err);

-                             // вызов функции;

у = cosh(x, eps, &еrr);      // & - взятие адреса

// обращение к еrr внутри функции;

rr = 0;                    // * - разадресация

          

В прототипе (и, конечно, в определении функции) явным образом указывается, что третьим параметром будет указатель на целое. При вызове на его место передается адрес переменной err. Чтобы внутри функции изменить значение этой переменной, применяется операция получения значения по адресу.

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

Для нашей программы передача вхлдных данных по константной ссылке выглядит так:

// прототип функиии;

double cosh(const double &x. const double &eps. int &err);

-                         // вызов функции;

у = cosh(x, eps, err);  /* обращение к x и eps внутри функции не изменяется */

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

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

int fputcdnt ch.  FILE *f);

 

записывает символ ch в поток f. При ошибке она возвращает значение EOF, иначе - записанный символ. В этом случае при необходимости передать в точку вызова * какие-либо другие результаты работы функции их передают через список параметров.

Часто в функциях библиотеки в случае возникновения ошибки применяется и более простое решение: при ошибке возвращается значение, равное нулю, хотя ноль может и входить в множество допустимых значений результата. В этом случае у программиста нет средств отличить ошибочное значение от правильного. Например, таким образом реализованы уже известные вам функции atoi, atol и atof. При невозможности преобразовать строку в число соответствующего типа они возвращают ноль, и то же самое значение будет выдано в случае, если в строке содержался символ 0.

Генерация исключения. Воспользуемся средством C++, называемым значениями параметров по умолчанию. Может оказаться неудобным каждый раз при вызове функции cosh задавать требуемую точность вычисления суммы ряда. Конечно, можно определить точность в виде константы внутри функции, задав максимальное допустимое значение, но иногда это может оказаться излишним, поэтому желательно сохранить возможность задания точности через параметры. Для этого либо в определении (если оно находится выше по тексту, чем любой вызов функции), либо в прототипе функции после имени параметра указывается его значение по умолчанию, например:

double cosh(double x. double eps - DBL EPSILON);

               

DBL EPSILON - это константа, определенная в файле <float.h>. Ее значение равно минимальному числу, которое, будучи прибавлено к единице, даст не равный единице результат. Теперь нашу функцию можно вызывать с одним параметром, к примеру:

у - cosh(x);

Функция может иметь несколько параметров со значениями по умолчанию. Они должны находиться в конце списка параметров.

Вариант прототипа функции с использованием параметра ошибки, а также значением точности по умолчанию выглядит так:

double cosh(const double x.int & err. const double eps = DBL EPSILON);

                

Соответствующим образом изменится и вызов функции. Указание перед параметром ключевого слова const в данном случае (при передаче по значению) применяется только для того, чтобы четко указать, какие из параметров являются входными. В случае передачи по ссылке указание const, кроме того, дает возможность передавать на месте этого параметра константу.

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

Пример 7.2. Передача в функцию имени функции

            

Назовем функцию вывода таблицы значений print tabl. Прежде всего надо определить ее интерфейс. Для того чтобы вывести таблицу, нашей функции потребуется знать диапазон и шаг изменения значений аргумента, а также какую, собственно, функцию мы собираемся вычислять. В функцию вычисления суммы ряда надо передавать точность, поэтому точность следует включить в список параметров вызывающей ее функции printtabl. Функция pri nt tabl не возвращает никакого значения, то есть перед ее именем надо указать void.

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

double (*fun)(double, double);

              

Здесь описывается указатель по имени fun на функцию, получающую два аргумента типа doubl e и возвращающую значение того же типа. Часто, если описание типа сложное, с целью улучшения читаемости программы задают для него синоним с помощью ключевого слова typedef:

typedef double (*Pfun)(double, double);

                

В этом операторе задается тип Pfun, который можно использовать наряду со стандартными типами при описании переменных. Таким образом, заголовок функции печати таблицы должен иметь вид:

void print_tabl(Pfun fun. double Xn, double Xk. double dX. double eps);

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

Листинг 7.2

#include <std1o.h>

#include <math.h>

typedef double (*Pfun)(const double, const double);  

void print_tabl(Pfun fun, const double Xn, const double Xk, const double eps);

double cosh(const double x, const double eps);

int main()

{

double Xn, Xk, dX, eps;

prlntf(Enter Xn, Xk, dX, eps \n");

 scanf("%lf%lf%lf%lf.” &Xn, &Xk, &dX, &eps);

 print tabl(cosh, Xn, Xk, dX, eps);

 return 0;

}

void print_tabl(Pfun fun, const double Xn, const double Xk, const double dX, const double eps)

{

pnintf(" ..............................\n");

printf("|       X       |           Y |\n");

printfC ................................ \n");

for (double x = Xn; x <=Xk; x += dX);

printf("|%9.2lf | %14.6g | \n”\,x,fun(x, eps));

 printf(" ................................\n);

}

double cosh(const double x, const double eps)

{

const int Maxlter = 500;

double ch = 1, у = ch;

for (int n = 0; fabs(ch) > eps; n++)

{

ch *= x * x /(2 * n + l)/(2 * n * 2);

у += ch;

     if  ( n> Maxlter) return 0;

   }

return  y;

}

          

Функция print tabl предназначена для вывода таблицы значений любой функции, принимающей два аргумента типа double и возвращающей значение того же типа.

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

   

Пример 7.3. Передача одномерных массивов в функцию

Даны два массива из n целых чисел каждый. Определить, в каком из них больше положительных элементов.

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

int n posit(const int *a, const int n);

             

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

Листинг 7.3

           

#include <iostream.h>

int n posit(const int *a, const int n);

int main()

{

int I, n;

cout « "Введите количество элементов:”:cin » n;

  

int *a = new int[n];

 int *b = new int[n];

cout « "Введите элементы первого массива: ";

for (i = 0; i < n; i++) cin » a[i];

cout « "Введите элементы второго массива: ";

for (i = 0; i < n; i++) cin » b[i];

if (n posit(a, n) > n posit(b, n))cout « " В первом положительных больше” « endl;

  else if(n posit(a, n) < n_posit(b, n)) cout « " Во втором положительных больше" « endl;

  else cout « " Одинаковое количество" « endl;

  

return 0;

int n posit(const int *a, const int n)

{

  int count = 0;

for (int i = 0; i < n; i++)

if (a[i] > 0) count++;

  return count;

}

}

В этой программе место под массивы выделяется в динамической области памяти, поскольку в задании не указано конкретное количество элементов. Однако функцию nposit можно без изменений применять и для «обычных» массивов, потому что для каждого из них имя тоже является указателем на нулевой элемент, только константным. Например, опишем массив из 10 элементов и инициализируем первые шесть из них (оставшимся будут присвоены нулевые значения):

int х[10] = {2, 3.  -1,  -10, 4,  -2};

cout « n_posit(x. 10); //будет выведено значение 3

           

Рассмотрим способ анализа результатов работы функции. Функция вызывается в составе выражения в условном операторе. Для перебора всех трех вариантов результата приходится вызывать ее для каждого массива дважды, что для больших массивов, конечно, нерационально. Чтобы избежать повторного вызова, можно завести две переменные, в которые записываются результаты обработки обоих массивов, а затем использовать эти переменные в условных операторах:

int n posit a = n posit(a. n), n posit b = n posit(b, n);

if (n posit a > n posit b) cout « " В первом положительных больше" « endl;

else if (n posit a < n posit b) cout « " Во втором положительных больше" « endl;

else cout « " Одинаковое количество" « endl;

          

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

   

Пример 7.4. Передача строк в функцию

Написать программу, определяющую, сколько чисел содержится в каждой строке текстового файла. Длина каждой строки не превышает 100 символов.

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

Отличие передачи в функцию строки от передачи обычного массива состоит в том, что можно не передавать отдельным параметром размерность строки, а определять конец строки внутри функции по нуль-символу. Для простоты предположим, что числа могут быть либо целые, либо вещественные с фиксированной точкой и непустой дробной частью. Распространить действие программы на другие виды чисел предоставляется вам в виде самостоятельного упражнения.

Листинг 7.4

#include <fstream.h>

#include <ctype.h>

 

int num num(const char *str);

int main()

{

ifstream fth("test.txt".ios::in|ios::nocreate);

if (!fin)

{

cout « "Нет файла test.txt" « endl;

return 0;

}

const int len = 101;

int i = 1;

char str[len];

while (fin.getline(str. len))

{

cout « "В строке  “« 1 « " содержится " « num num(str) « " чисел " « endl;

i++;

}

return 0;

}

int num num(const char *str)

{

 int count = 0;

 while (*str)

{

if (isdigit(*str) && ! isdigit(*(str + 1))) && *(str + 1) != '.') count++;

str++;

}

return cout;

}

}

 

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

         

Пример 7.5. Передача двумерных массивов в функцию

    

Написать программу, определяющую, в какой строке целочисленной матрицы т х п находится самая длинная серия одинаковых элементов. 

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

Листинг 7.5

#include <fstream.h>

int ser equals(int **a, const int m, const int n);

int main()

{

if stream fin (."matrix.txt", los::in | ios :: nocreate);

if (!fin) 

{

cout « "Нет файла matrix.txt" « endl;

   return 0;

}

int m, n, 1, j;

fin » m » n;

int **a = new int *[m];     // выделение памяти

for (d = 0; i < m; i++)

a[i] = new int [n];

for (i = 0; i < m; i++)        // ввод нассива

for (j = 0; j < n)       fin » a[i][j];

 int line = ser_equals(a, m, n);   // вызов функции

if (line >= 0) cout « " Самая длинная серия в строке " « line;

  else cout « " Серий одинаковых элементов нет ";

return 0;

}

int ser_equals(int **а, const int m, const int n)

{

int I, j,  count, line = -1, maxcount = 0;

 for (1 - 0; 1 < m; 1++)

{

count = 0;

for (j = 0; j < n - 1; j++)

{

   if (a[i][j] == a[l][j + 1])

   else

{

if (count > maxcount)

{

maxcount = count;

line = i;

}

count = 0;

}

}

if (count > maxcount)

{

maxcount = count;

line = i;

}

}

}

return line;

Алгоритм работы функции прост: в каждой строке выполняется сравнение соседних элементов (оператор 2). Если они равны, мы находимся внутри серии, при этом увеличиваем ее текущую длину. Она накапливается в переменной count, которая обнуляется перед обработкой каждой строки (оператор 1). Если же элементы не равны, это означает либо окончание серии, либо просто одиночный элемент (оператор 3). В этом случае надо посмотреть, не является ли данная серия самой длинной из рассмотренных и, если да, то запомнить ее длину и номер строки, в которой она встретилась (оператор 4). Для подготовки к анализу следующих серий в этой же строке надо обнулить счетчик count. Аналогичная проверка после цикла просмотра строки (оператор 5) выполняется для серии, которая расположена в конце строки, поскольку в этом случае ветвь else выполняться не будет.

Если в массиве нет ни одной серии одинаковых элементов, функция вернет значение, равное -1.

               

Пример 7.6. Передача структур в функцию

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

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

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

Листинг 7.6

#include <stdio.h>

#include <string.h>

#include <stdlib.h>        //#include <windows.h>

const int l name = 30;

char name[l_name + 1];

int birth year;

float pay;

Man read data();

int append2binfilet (const Man &man, const char* filename);

int print from bin(const char * filename);

int main()

{

bool contin;

char y n[2];

char filename[] = "dbase.bin";

do

{

contin = false;

if (append2binfile(read data(), filename) != 0)

{

    puts(" Ошибка при записи в файл ");

    return 0;

}

puts(" Продолжить (у/n)?”);

   gets(y i);

if ((y n[0] == 'у')||(у n[0] == 'Y'))contin = true;

}

while (contin); print from bin(fi1ename);

 return 0;

}

int append2binfile(const Man &man, const char* filename)

{

 FILE *fout;

if ((fout-fopen(filename, "ab"))NULL) return 1;

 

int success = fwrite(&man, sizeof(man), 1, fout), fclose(fout);

if (success == 1) return 0;

else return 2;

}

int print from_bin(const char * filename)

{

int num; Man man; FILE *f;

if ((f = fopenCfilename, "rb")) == NULL ) return 1;

 fseef((f. 0. SEEKEND);

 int n record - ftell(f) / sizeof (man);

while (true)

{

puts("Введите номер записи или -1; ");

scanf("&i", &num);

  if (num < 0 || num >= n record) break ;

fseek(f. num * sizeof(man). SEEK SET) ;

fread(&man, sizeof(man), 1, f);

                //  CharToOem(man,name, man,name);

        printf("%30s%5i%10,2f\n,  man.name, man.birth year. man.pay);

return 0;

}

Man read_data()

{

Man man;

char buf[80];

char name[l_name + 1];

puts("Введите фамилию И.О. ");

gets(name);

if (strlen(name) < 1_name)

for (int i = strlen(name); i < l name; i++)

name[l_name] = 0;

                        // OemToChar(name. name);

strncpy(man.name. name. l_ name + 1);

do 

{

puts("Введите год рождения “);     

gets(buf)

  while ((man.birth_year = atoi(buf)) ==0);

do

{

puts(“Bвeдитe оклад ");

gets(buf);

}

  while (!(man.pay =atof(buf)));

return man;

 }

}

}

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

Следует обратить внимание на то, как в этой функции выполняется проверка правильности ввода числовой информации. Чтение выполняется в буферную строку, которая затем преобразуется с помощью функций atoi () и atof () в числа. Если функции возвращают 0, преобразование выполнить не удалось (например, вместо цифр были введены буквы), и информация запрашивается повторно. Условие повторения циклов 3 и 4 записано в двух разных вариантах, чтобы вы сами могли оценить, какой из них вам более понятен (профессионалы предпочли бы второй, более лаконичный вариант).

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

     

Пример 7.7. Рекурсивные функции

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

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

Одна из возможных версий программы сортировки приведена ниже.

Листинг 7.7

include <iostream.h>

void qsort(float* array, int left, int right);

int main()

{

const int n = 10;

 float arr[n];

 int 1, 1, r;

cout « "введите элементы массива:”;

  for (1 = 0; i < n; i++)  cin » arr[i];

i = o;r = n - 1;         /* левая и правая границы начального фрагмента*/

qsort(arr, 1, r);                  // 1

for (i = 0; i < n; i++) cout « arr[i] « ‘  ‘;  

return 0;

}

void qsort(float* array, int left, int right)

{

int i = left, j = right;

 float middle = array[(left + right) / 2];

 float temp;

while (i < j)    

{

while (array[i] < middle) i++;

  while (middle < array[j]) j--;

  if (i <= j)

{

temp = array[i];

array[i] = array[j];

array[j] = temp;

i++;

j--;

}

}

if (left < j) qsort(array, left, j);

 if (i < right) qsort(array, I, right);

}

Процедура разделения реализована здесь в виде рекурсивно вызываемой функции qsort(), в теле которой есть два обращения к самой себе: в операторе 2 - для сортировки левой половинки текущего фрагмента, и в операторе 3 - для сортировки его правой половинки.

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

   

Пример 7.8. Многофайловый проект - форматирование текста

Написать программу форматирования текста, читаемого из файла unformt.txt и состоящего из строк ограниченной длины. Слова в строке разделены произвольным количеством пробелов. Программа должна читать входной файл по строкам, форматировать каждую строку и выводить результат в выходной файл formatd-.txt. Форматирование заключается в выравнивании границ текста слева и справа путем равномерного распределения пробелов между соседними словами, а также в отступе с левой стороны страницы на margin позиций, то есть результирующий текст должен находиться в позициях margin + 1 .. margin + maxljine. Кроме этого, программа должна подсчитать общее количество слов в тексте.

Алгоритм решения задачи:

  1.  Открыть входной файл.
  2.  Читать файл построчно в текстовый буфер line, попутно удаляя возможные
    пробелы в начале строки (до первого слова).
  3.  Для каждой строки 11 пе выполнить следующие действия:

- Вычислить величину интервала (количество пробелов), которую необходимо обеспечить между соседними словами для равномерного распределения слов в пределах строки.

- Вывести каждое слово из строки 1 i пе в выходной файл, вставляя между словами необходимое количество пробелов и одновременно увеличивая счетчик слов на единицу.

4. После обработки последней строки входного файла вывести на экран значение счетчика слов и закрыть выходной файл.

Разбиение на подзадачи.

В результате детализации описанного алгоритма определяем спецификации нужных нам функций:

J void Deflnter (const char* pline, int & base int. int & add int. int & inter)

определяет для строки, на которую указывает pline, количество межсловных промежутков inter, требуемую величину основного интервала base int для каждого промежутка (количество пробелов) и величину дополнительного интервала add int, определяемую как остаток от деления общего количества пробелов в строке на количество межсловных промежутков; последняя величина должна быть равномерно распределена путем добавления одного пробела в каждый из первых add i nt промежутков;

void GetLine  (FILE* finp.   char* pline)

читает очередную строку из входного файла в массив символов с адресом pi ine, ликвидируя при этом пробелы в начале строки;

void Putlnterval (FILE* fout. const int k)

выводит очередной интервал, состоящий из к пробелов;

 int PutWord (FILE*fout,const char*pline.const int startpos)

выводит очередное слово в выходной файл, начиная с позиции startpos текущей строки pi ine; возвращает номер позиции в строке pi i ne, следующей за последним переданным символом, или 0 - если достигнут конец строки;

int SearchNextWord'(const char*pline.const int curpos)

возвращает номер позиции, с которой начинается следующее слово в строке pi 1 пе, или 0, если достигнут конец строки (поиск начинается с позиции curpos).

Разбиение на модули.

Наша программа будет располагаться в двух исходных файлах: task7_7.cpp - с функцией main, edit. cpp - с реализацией перечисленных выше функций, а также заголовочный файл edit.h с интерфейсом этих функций. Ниже приводится содержимое этих файлов.

Листинг 7.8

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

// Файл Task7_7.cpp

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include "edit.h"

// Глобальные переменные 

const int maxljine = 63;

const int margin = 5;

int main()

{

FILE* finp;

 FILE* tout;

char line[maxl_line + 1];

int   b i, a i, start, next, inter;

int   nword = 0;

printf("Работает программа Task7 7.\n");

 if(!(finp = fopen("unformt.txt",”W”)))

{

  printf("Файл unformt.txt не найден.\n”);

  exit(0);

}

printf("Читается файл unformt.txt \n");

 if(!(fout = fopen(“formatd.txt", "w")))

{

  printf("Фaйл formatd.txt не создан.\n”);

  exit(O);

}

printf("Выполняется запись в файл formatd.txt.\n");

while(GetLine(finp. line))

{

  DefInter (line, b i, a_i, inter);

  PutInterval(fout, margin);

next = PutWord(fout, line, start, nword);

for (int i = 0; i < inter; i++)

{

start = SearchNextWord(line. next);

   Putlnterval(fout.. b i);

   if (a j) { a i--; Putlnterval(fout. 1);

}

  next = PutWord(fout. line, start, nword);

if (!next) break;

}

fprintf(fout. “\n");

}

printf (“\nКоличество слов - %d\n”, nword);

fclose(fout);

printf("Работа завершена \n");

return 0;

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

// Файл Edit.h

// Прототипы функций

void DefInter(const char* pline, Int& base int, int& add int,

int& inter);

int     GetLine(FILE*. char*);

void    Putlnterval(FILE*. const int);

int   PutWord(FILE*. const char*, const int. int&);

int     SearchNextWord(const char*, const int);

// Глобальные переменные 

extern const int maxl line;

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

// Файл Edit.cpp

#include <stdio.h>

#include <string.h>

#include "edlt.h"

int GetLine(FILE* finp. char* pline)

{

int i = 0;

 char c;

while ((c = fgetc(finp)) == ' ') i++;

if(c == EOF) return 0;

fseek(finp. -1, SEEK CUR);

fgets(pline. Maxl line -i + 1, finp);

pline[strlen(pline) - 1] = 0;

return 1;

}

int SearchNextWord(const char* pline, const int curpos)

{

 int i = curpos;

 while(pline[i] !='')

{

  if (pline[i] ==;\n”) return 0:

i++;

}

while (pline[i] == ’ ‘&& pline[i + 1] == ' ') i++;

return i + 1;

}

void DefInter(const char* pline. int& base int. int& add int. int& inter)

{

int к = 0, end;

end = strlen(pline) - 1;

while ((pline[end] == ' ') || (pline[end] == ’\n') || (pline[end] == '\r')) end--;

inter = 0;

for (unsigned int i= 0; i < end; i++)

{

  if (pline[i] == ' ')

{

if (pline[i + 1] != ' ') inter++;

}

}

int blank amount * к + maxl line - end;

 if (!k)

{

base int = 0;

add int = 0;

}

 else

{

base int = blank amount / inter;

  addjnt = blank amount % inter;

}

return;

}

int PutWord (FILE* fout, const char* pline, const int startpos, int& n)

{

int i = startpos;

 char c;

n++;

while ((c - pline[i++]) !=' ‘)

{

  fprintf(fout, "%c", c);

if ((c = '\n'> || (c == '\0'))

{

i = 0;

break;

}

}

return i - 1;

void Putlnterval(FILE* fout, const int k)

{

  for (int i=0; i<k; i++) fprintf(fout, " );

return;

}

}

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

         

Имена функций мы здесь записали (для разнообразия) в стиле Microsoft, с использованием прописных букв для выделения осмысленных частей имени. Константу maxl 1 пе следует задавать большей, чем максимальная длина строки исходного файла. В качестве самостоятельного упражнения измените программу так, чтобы можно было форматировать текст и в более узкую колонку, чем в исходном файле.

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

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

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

void print( char* str. const int i. const int j)

{

cout « str « ' I' « oct « setw(4) « i « ' |' « setw(4) « j « ' |’ « endl;

}

void print(float mas[], const int n)

{

 cout « "Массив:"  « endl;

 cout.setf(ios::fixed);

 cout.precision(2);

 for (int i = 0; i < n; i++)

{

cout « mas[i] « "    ";

if ((i + 1) % 4 == 0) cout « endl;

}

cout « endl;

}

void print(Man m)

{

cout.setf(i os::fixed);

 cout.precision(2);

cout « setw(40) « m.name « ' ‘ « m.birth year  <<' ‘<< m.pay « endl;

}

В первой из этих функций на экран выводятся строка и два целых числа в восьмеричной форме, разделенных вертикальными черточками для читаемости. Под каждое число отводится по 4 позиции (действие манипулятора setw распространяется только на ближайшее выводимое поле).

Во второй функции для вывода вещественных значений по четыре числа на строке задается вид вывода с фиксированной точкой и точностью в два десятичных знака после запятой. Для этого используются методы установки флагов setf, установки точности precision и константа f1xed, определенная в классе 1os. Точность касается только вещественных чисел, ее действие продолжается до следующей установки.

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

print("После цикла ", 1, п);

print(a. n);

print(m);

           

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

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

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

Неоднозначность может также возникнуть из-за параметров по умолчанию и ссылок. Рассмотрим создание перегруженных функций на примере.

   

Пример 7.9. Перегрузка функций

Написать программу, которая для базы сотрудников выдает по запросу список сотрудников либо родившихся раньше заданного года, либо имеющих оклад больше введенного с клавиатуры.

Варианты выборки из базы по различным запросам оформим в виде перегруженных функций. Мы от природной лени и для простоты рассматриваем базу с минимальным количеством полей; в реальных ситуациях их может быть гораздо больше, соответственно, больше будет и вариантов перегруженных функций. Также оформим в виде отдельной функции чтение базы из файла - и для лучшего структурирования программы, и для того, чтобы в случае необходимости было легче заменить эту функцию на другую, например на чтение из бинарного файла.

Листинг 7.9

#include <fstream.h>

#include <str1ng.h>

#include <stdlib.h>

#include <iomanip.h>

const int l name = 30, l year = 5, l pay = 10, l buf = l name + 1 year + l pay;

struct Man

{

int birth_year;

 char  name[l name + 1];

 float pay;

int read dbase(const char * filename, Man dbase[], const int l dbase, int &n record);

 void printtMan m);

void select(Man dbase[]. const int n record. const int year);

 void select(Man dbase[]. const int n ecord. const float pay);

int main()

{

const int l dbase = 100;

  Man dbase[l dbase];

  int n record = 0;

if(read dbase(“txt6.txt”.dbase.1 dbase.n record) !=0) return 1;

int option;

int year;

float pay;

do 

{

cout <<”---------------------“<< endl;

cout << ”1 - Cведения по году рождения“ << endl;

cout <<”2 - Сведения по окладу“<<endl;

cout <<”3 - выход“<< endl;

cin >> option;

switch (option)

{

Case1: << ”Введите год” >> cin >> year;

select(dbase.n_record.year); break;

Case2: << ”Введите оклад” >> cin >> pay;

select(dbase.n_record.pay);

break;

Case 3: return 0;

default: cout << ”Надо вводить число от 1 до 3” << endl;

}

}

while(true);

return 0;

}

Void select(man dbase[], const int n record, const int year)

{

Cout << ”Ввод сведений по году рождения” << endl;

bool success = false;

for(int i = 0; i < n record; i++)

if(dbase[i],birth year >= year)

{

Print(dbase[i]);

success = true;

}

if (!success) Cout <<”Таких сотрудников нет”<<endl;

Void select(man dbase[], const int n_record, const float pay)

{

cout << ”Ввод сведений по окладу” << endl;

bool success = false;

for(int i = 0; i < n record; i++)

if(dbase[i],birth pay >= pay)

{

print(dbase[i]);

success = true;

}

if (!success) cout <<”Таких сотрудников нет”<<endl;

Void print (Man m)

{

cout.setf(iod::fixed);

cout.precision(2);

cout « setw(40) « m.name « ' ' « m.birth_year « ' ' « m.pay « endl;

}

int read dbase(const char * filename. Man dbase[]. const int l dbase. int &n record)    {

char buf [l buf +1];

if stream fin(filename. ios::in | ios:mocreate);

if (!fin )

{

cout « "Нет файла " « filename « endl;

return 1;

}

int i = 0;

while "(fin.getline(buf, 1 buf))

{

   strncpy(dbase[i]name,buf, l name);

    dbase[i].name[l name]  = '\0';

dbase[i].birth year = atoi(&buf[l name]);

dbase[i].pay = atof(&buf[l_name + l year]);

    i++;

if (i > l dbase)

{

cout « "Слишком длинный файл";

return 2; 

}

}

n record = i;

  fin.close();

  return 0;

}

Правила описания перегруженных функций:

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

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

 3. Функции не могут быть перегружены, если описание их параметров отличается только модификатором const или использованием ссылки.

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

Шаблон функции определяется следующим образом:

template <class Туре> тип имя ([список параметров])

{

/* тело функции */

}

            

Идентификатор Туре, задающий так называемый параметризованный тип, может использоваться как в остальной части заголовка, так и в теле функции. Параметризованный тип - это всего лишь фиктивное имя, которое компилятор автоматически заменит именем реального типа данных при создании конкретной версии функции. В общем случае шаблон функции может содержать несколько параметризованных типов <class Typel. class Type2. class ТуреЗ, ... >.

Процесс создания конкретной версии функции называется инстанцированием шаблона или созданием экземпляра функции. Возможны два способа инстанцирования шаблона: а) явный, когда объявляется заголовок функции, в котором все параметризованные типы заменены на конкретные типы, известные в этот момент в программе, б) неявный, когда создание экземпляра функции происходит автоматически, если встречается фактический вызов функции.

Шаблоны тоже можно перегружать, причем как шаблонами, так и обычными функциями.

Пример 7.10. Шаблоны функций

Написать программу, которая определяет максимальные элементы в одномерных массивах различных арифметических типов.

Поиск максимума - весьма распространенная задача. Для этого достаточно простейшего шаблона с одним параметром-типом. В саму функцию будет передаваться два аргумента: указатель на массив и длина этого массива.

Листинг 7.10

#include <1ostream.h>

#include <str1ng.h>

template <class T> T Max(T *b. int n);

int main()

{

const int n = 20;

 int I, b[n];

cout « "Введите " « n « " целых чисел:" « endl;

 

for (i = 0; i < n; i++) cin » b[i];

 cout « Max(b, n) « endl;

double a[] = {0.22, 117.2, -0.08, 0.21, 42.5};

 cout « Max(a, 5) « endl;

 

char *str = "Sophisticated fantastic template";

 cout « Max(str. strlen(str)) « endl;

 

return 0;

}

template <class T> T Max(T *b, int n)

{

 int imax = 0;

 for (int i = 1; i < n; i++)

if (b[i] > b[imax]) imax = i;

 return b[imax];

}

Шаблон функции имеет имя Мах. После ключевого слова tempi ate в угловых скобках перечисляются все параметры шаблона. В данном случае параметр один. При инстанцировании шаблона (в данном случае - неявном), то есть когда компилятор будет создавать конкретный вариант функции, этот тип будет заменен конкретным стандартным или пользовательским типом. Соответствие устанавливается при вызове функции либо по типу аргументов, либо по явным образом указанному типу. Например, последний вызов функции можно записать так:

cout « Max <char> (str, strlen(str));

           

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

Аналогично обычным параметрам функции, можно задавать значение параметра шаблона по умолчанию.

При работе с многофайловым проектом нужно не забывать, что если какой-то шаблон функции имеет инстанцирование в нескольких исходных файлах, то определение этого шаблона должно повторяться в каждом из этих файлов. Поэтому обычно определение шаблона выносят в заголовочный файл и подключают его в нужных местах директивой #include.

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

1. Проработать примеры, приведенные в лабораторной работе.

2. Выполнить задания согласно варианта индивидуального задания №1 лабораторной работы №6, оформив каждый пункт задания в виде функции. Все необходимые данные для функций должны передаваться им в качестве параметров. Использование глобальных переменных в функциях не допускается.

3. Выполнить задания согласно варианта индивидуального задания №1 лабораторной работы №6, оформив каждый пункт задания в виде шаблона функции. Все необходимые данные для функций должны передаваться им в качестве параметров. Использование глобальных переменных в функциях не допускается.

4. В соответствии с вариантом, используя прямую рекурсию, написать и выполнить программу. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Вариант:

  1.  Напечатать в обратном порядке последовательность чисел, признаком конца которой является 0.
  2.  Для n = 12 найти числа Фибоначчи. Числа Фибоначчи: F(0) = 1, F(1) = 1 F(n) = F(n - 2) + F(n - l)
  3.  Даны целые числа m и n, где 0  m  n, вычислить, используя рекурсию, число сочетаний С(n, m) по формуле: ,  при 0  m  n. Воспользовавшись формулой  можно проверить правильность результата.
  4.  Опишите рекурсивную функцию, которая по заданным вещественному х и целому п вычисляет величину хn согласно формуле:

  1.  Задана последовательность положительных чисел, признаком конца которых служит отрицательное число. Используя рекурсию, подсчитать количество чисел и их сумму.
  2.  Дан вектор X из n вещественных чисел. Найти минимальный элемент вектора, используя вспомогательную рекурсивную функцию, находящую минимум среди последних элементов вектора X, начиная с no.
  3.  Напишите рекурсивную функцию для нахождения биномиальных коэффициентов (для заданного М  i  j > 0 вычислите все ):

  1.  Напишите программу вычисления функции Аккермана для всех неотрицательных целых аргументов т и п:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

 

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Напишите рекурсивную функцию, которая вычисляет по следующей формуле: 

п = 0,1,2, … . За ответ принять приближение, для которого выполняется условие , где  = 0,0001.

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

  1.  Для заданных границ интегрирования а и b вычислите значение определенного интеграла следующего вида:

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

1. Названия лабораторной работы.

2. Цели и содержания лабораторной работы.

3. Ответов на контрольные вопросы лабораторной работы.

4. Формулировки индивидуальных заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

1. Что представляет собой функция в С++? Что нужно для ее использования?

2. Приведите пример заголовка функции.

3. Что включает в себя определение функции?

4. В чем отличие функции от других программных объектов?

5. Каким образом происходит вызов функции?

6. Охарактеризуйте существующие способы решения проблемы получения из подпрограммы признака ее аварийного завершения.

7. Каков механизм передачи параметров в функцию?

8. Способы передачи входных данных.

9. Что представляет собой средство C++, называемое значениями параметров по умолчанию?

10. Каким образом происходит передача в функцию имени функции?

11. Каким образом происходит передача одномерных массивов в функцию?

12. Каким образом происходит передача строк в функцию?

13. Каким образом происходит передача двумерных массивов в функцию?

14. Каким образом происходит передача структур в функцию?

15. Какая функция называется рекурсивной? Преимущества и недостатки рекурсии.

16. Что называется перегрузкой функций? Перечислите правила описания перегруженных функций.

17. Область применения шаблонов.

18. Что такое инстанцирование? Способы инстанцирования шаблона.

Пример выполнения лабораторной работы №7:

1. Индивидуальное задание №1:

1.1. Постановка задачи:

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

Задача: построить результат сглаживания заданной вещественной матрицы размером 10 на 10. В сглаженной матрице найти сумму модулей элементов, расположенных выше главной диагонали. Ввод и вовод данных в программе осуществить с помощью файла.

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

1.2. UML-диаграмма:

1.3. Листинг программы:

// Лабораторная работа №7

// Индивидуальное задание №1

#include <iostream>

#include <fstream>

#include <iomanip>

using namespace std;

int sort_array(int ncol, int nrow);

int sort_array(int ncol, int nrow)

{

 ifstream fin("input.txt",ios::in);

 if (!fin)

 {

      cout << "Can't find input.txt" << endl;

 return 1;

 }

  

   ofstream fout("output.txt");

   if (!fout)

 {

      cout << "Write falure: check permission" <<endl;

 return 1;

 }

  

 int k, l, k1, l1, counter, i, j;

 double sum,sumdiag = 0;

 double a[nrow][ncol];

cout << "Чтение данных из файла..." << endl;

 for(i = 0; i < nrow; i++)

 for(j = 0; j < ncol; j++)

 fin >> a[i][j];

        

 cout << "Обработка данных..." << endl;

 counter = 0;sum = 0; l = 0; l1 = 0;

 double m[nrow][ncol];

   //calculate neighbour' sum & sum diagonalies

 for(i = 0; i < nrow; i++)

 for(j = 0; j < ncol; j++)

 {

 k = i -1;

 k1 = i + 1;

 if (k<0) k++;

 if (k1 > nco l- 1) k1--;

 if ((j <= ncol - 1)&&(j >= ncol - i)) sumdiag = sumdiag+a[i][j];

 for(k; k <= k1; k++)

 {

  l = j - 1;

l1 = j + 1;

if (l < 0) l++;

if (l1 > ncol - 1) l1--;

for(l; l <= l1; l++)

if ((k == i) && (l == j)) continue;

else

{

sum = sum + a[k][l];

counter++;

 }

}

m[i][j] = (float) sum/counter;

sum = 0;

counter = 0;

 }

 for(i = 0; i < nrow; i++)

 {

 for(j = 0; j < ncol; j++)

 fout << setw(5) << left << setprecision(3) << m[i][j] <<"  " << endl;

}

fout << endl;

fout << "Сумма эл-ов ниже главной диагонали: " << setw(5) << setprecision(9) << sumdiag << endl;

 return 0;

}

int main()

{

   const int nrow = 10;

   const int ncol = 10;

   if (!sort_array(ncol,nrow))

 {   

    cout<<"Запись результатов обработки в файл..."<<endl;

    cout<<"Обработка успешно закончена!"<<endl;

    return 0;

   }

   else

   {

     cout<<"Ошибка обработки данных!"<<endl;

     return 1;

   }

}

1.4. Результаты работы программы:

Рисунок 7.1 – Файл для входных данных

Рисунок 7.2 – Файл для выходных данных

Рисунок 7.3 – Вывод программы на экран

2. Индивидуальное задание №2:

2.1. Постановка задачи:

Выполнить задания согласно варианта индивидуального задания №1 лабораторной работы №7, оформив в виде функций законченные последовательности действий. Все необходимые данные для функций должны передаваться им в качестве параметров. Использование глобальных переменных в функциях не допускается.

2.2. UML-диаграмма:

2.3. Листинг программы:

// Лабораторная работа №7

// Индивидуальное задание №2

#include <iostream.h>

#include <fstream>

#include <string.h>

using namespace std;

int readfile(string fileName)

{

   ifstream fin(fileName.c_str(),ios::in);

   if (!fin)

{

 cout << "Ошибка открытия файла" << endl;

 return 1;

 }

int nword;

  

   cout << "Введите искомое число слов в предложении: ";

   cin >> nword;

   

 fin.seekg(0,ios::end);

   int len = fin.tellg();

   char *buf = new char [len + 1];

   fin.seekg(0,ios::beg);

   fin.read(buf,len);

   buf[len] = '\0';

   int l_beg = 0, i = 0, n = 0, j = 0;

   bool exist;

   exist = false;

   while (buf[i])

   {

      if (buf[i] == ' ') n++;

      if (buf[i] == '.')

      {

         n++;

         if (n == nword)

         {

             for(j = l_beg; j <= i; j++)

               cout << buf[j];

             exist = true;

             cout << endl;

         }

         l_beg = i + 2;

         i = i + 2;

         n = 0;

      }

      i++;

   }

   if (!exist)

 cout << "Таких предложений не найдено" << endl;

   fin.close();

   return 0;

}

int main()

{

   setlocale( LC_ALL, "Russian" );

   cout <<"\n"

        <<"\tЛабораторная работа № 7\n"

        <<"\n"

        <<"------------------------------------------\n"

        <<"Задание: Считать текст из файла. Вывести на экран только предложения, \n"

        <<"\tсостоящие из заданного количества слов\n"

        <<"-----------------------------------------\n";

   string str;

   cout << "Какой файл открыть? ";

   cin >> str;

   readfile(str);

   return 0;

}

2.4. Результаты работы программы:

Рисунок 7.4 – Файл для входных данных

Рисунок 7.5 – Вывод программы на экран

3. Индивидуальное задание №3:

3.1. Постановка задачи:

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

3.2. Листинг программы:

// Лабораторная работа №7

// Индивидуальное задание №3

#include <iostream>

#include <fstream>

#include <iomanip>

using namespace std;

template <class T> T sort_array(T ncol, T nrow)

{

   ifstream fin("input.txt",ios::in);

   if (!fin)

 {

      cout << "Can't find input.txt" << endl;

 return 1;

 }

   ofstream fout("output.txt");

   if (!fout)

 {

      cout << "Write falure: check permission" <<endl;

 return 1;

 }

  

 int k, l, k1, l1, counter, i, j;

double sum, sumdiag = 0;

double a[nrow][ncol];

cout << "Чтение данных из файла..." << endl;

for(i = 0; i < nrow; i++)

for(j = 0; j < ncol; j++) fin >> a[i][j];

        

 cout << "Обработка данных..." << endl;

 

 counter = 0; sum = 0; l = 0; l1 = 0;

double m[nrow][ncol];

 //calculate neighbour' sum & sum diagonalies

 for(i = 0; i < nrow; i++)

 for(j = 0; j < ncol; j++)

 {

k = i - 1;

k1 = i + 1;

if (k<0) k++;

if (k1 > ncol - 1) k1--;

 if ((j <= ncol - 1)&&(j >= ncol - i))

  sumdiag = sumdiag + a[i][j];

 for(k; k <= k1; k++)

 {

l = j - 1;

l1 = j + 1;

if (l < 0) l++;

if (l1 > ncol - 1) l1--;

for(l; l <= l1; l++)

if ((k == i)&&(l == j)) continue;

  else

  {

   sum = sum + a[k][l];

   counter++;

  }

 }

 m[i][j] = (float) sum/counter;

 sum = 0;

 counter = 0;

 }

 for(i = 0; i < nrow; i++)

{

for(j = 0; j < ncol; j++)

fout << setw(5) << left << setprecision(3)

             << m[i][j] << "  ";

 fout << endl;

 }

 fout << endl;

 fout << "Сумма эл-ов ниже главной диагонали: "

        << setw(5) << setprecision(9) << sumdiag <<endl;

 return 0;

}

int main()

{

   const int nrow = 10;

   const int ncol = 10;

   sort_array(ncol,nrow);

   cout << "Запись результатов обработки в файл..."

  << endl;

   cout << "Обработка успешно закончена!" << endl;

   cout << "Ошибка обработки данных!" << endl;

   return 0;

}

4. Индивидуальное задание №4:

4.1. Постановка задачи:

Дан вектор X из n вещественных чисел. Найти минимальный элемент вектора, используя вспомогательную рекурсивную функцию, находящую минимум среди последних элементов вектора X, начиная с no.

4.2. UML-диаграмма:

4.3. UML-диаграмма для функции float sort_array(float array[],int size,int pos)

4.4. Листинг программы:

// Лабораторная работа №7

// Индивидуальное задание №4

#include <iostream>

#include <math.h>

using namespace std;

float sort_array(float array[], int size, int pos)

{

   float min = array[pos];

   pos++;

   for(int i = pos; i < 7; i++)

 {

      if (array[i] < min) min = array[i];

      sort_array(array, size, pos);

   }

   return min;

}

int main()

{

   int size = 0;

   cout << "Введите количество элементов вектора X: ";

   cin >> size;

  

   float *array = new float [size];  

   for(int i = 0; i < size; i++)

{

      cout << "Введите " <<i<< " элемент вектора X: ";

      cin >> array[i];

   }

  

   int pos = 0;

   cout << "С какого элемента начать сортировку? ";

   cin >> pos;

  

   cout << endl;

   cout << "Ввод данных закончен...\n";

   cout << "Идет процесс сортировки...\n";

  

   cout << "Минимальный элемент вектора X: "

        << sort_array(array,size,pos) << "\n";

   return 0;

}

4.5. Результаты работы программы:

Лабораторная работа №8.
Строки и файлы в языке C++

Цель работы и содержание: закрепление знаний о строках и файлах, составление программ со строками и файлами.

Ход работы

Основные сведения о строках в языке С++. В C++ есть два вида строк: С-строки и класс стандартной библиотеки C++ string. С-строка представляет собой массив символов, завершающийся символом с кодом О. Класс string более безопасен в использовании, чем С-строки, но и более ресурсоемок.

Описание строк. Память под строки, как и под другие массивы, может выделяться как компилятором, так и непосредственно в программе. Длина динамической строки может задаваться выражением, длина нединамической строки должна быть только константным выражением. Чаще всего длина строки задается частным случаем константного выражения - константой. Удобно задавать длину с помощью именованной константы, поскольку такой вариант, во-первых, лучше читается, а во-вторых, при возможном изменении длины строки потребуется изменить программу только в одном месте:

const int len_str = 80;

char str [len_str];

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

char a[100] = "Never trouble trouble";

Если строка при определении инициализируется, ее размерность можно опускать (компилятор сам выделит память, достаточную для размещения всех символов строки и завершающего нуля):

char а[] = "Never trouble trouble"; // 22 символа

Для размещения строки в динамической памяти надо описать указатель на char, а затем выделить память с помощью new или mall ос (первый способ предпочтительнее):

char *p = new char [m];

char *q - (char *)malloc( m * sizeof(cnar));

Естественно, что в этом случае длина строки может быть переменной и задаваться на этапе выполнения программы. Динамические строки, как и другие динамические массивы, нельзя инициализировать при создании. Оператор

char *str = "Never trouble trouble"

создает не строковую переменную, а указатель на строковую константу, изменить которую невозможно.

Ввод-вывод строк. Для ввода-вывода строк используются как уже известные нам объекты cin и cout, так и функции, унаследованные из библиотеки С.  Рассмотрим сначала первый способ:

#include<iostream.h>

int main()

{

const int n = 80;

char s[n];

cin>>s;

cout << s << endl;

return 0;

}

Строка вводится точно так же, как и переменные известных нам типов. Если запустите программу и ввести строку из нескольких слов, выводится только первое слово. Это связано с тем, что ввод выполняется до первого пробельного символа (то есть пробела, знака табуляции или символа перевода строки '\n').

Можно ввести слова входной строки в отдельные строковые переменные:

#include <iostream.h>

int main()

{

const int n = 80;

char s[n], t[n], r[n];

cin >> s >> t >> r;

cout << s << endl << <t << endl << r << endl;

return 0;

}

Если требуется ввести строку, состоящую из нескольких слов, в одну строковую переменную, используются методы getline или get класса istream, объектом которого является cin. Синтаксис вызова метода - после имени объекта ставится точка, а затем пишется имя метода:

#include<iostream.h>

int main()

{

const int n = 80;

char s[n];

cin.getline(s. n); cout << s << endl;

cin.get(s. n); cout << s << endl;

return 0;

}

Метод getl 1 пе считывает из входного потока п - 1 символов или менее (если символ перевода строки встретится раньше) и записывает их в строковую переменную s. Символ перевода строки также считывается (удаляется) из входного потока, но не записывается в строковую переменную, вместо него размещается завершающий 0. Если в строке исходных данных более n-1 символов, следующий ввод будет выполняться из той же строки, начиная с первого несчитанного символа.

Метод get работает аналогично, но оставляет в потоке символ перевода строки. В строковую переменную добавляется завершающий 0.

Никогда не обращайтесь к разновидности метода get с двумя аргументами два раза подряд, не удалив \n из входного потока. Например:

cin.get(s. n);           //1-считывание строки                                       

cout << s << endl;       //2-вывод строки                                      

cin.get(s. n);           //3-считывание строки                                          

cout << s << endl;       //4-вывод строки                                     

cin.get(s. n);           //5-считывание строки                                         

cout << s << endl;       //6-вывод строки                                    

cout << "Конец-делу венец" << endl;           //7

При выполнении этого фрагмента вы увидите на экране первую строку, выведенную оператором 2, а затем завершающее сообщение, выведенное оператором 7. Какие бы прекрасные строки вы ни ввели с клавиатуры в надежде, что они будут прочитаны операторами 3 и 5, метод get в данном случае «уткнется» в символ \n, оставленный во входном потоке от первого вызова этого метода (оператор 1). В результате будут считаны и, соответственно, выведены на экран пустые строки (строки, содержащие 0 символов). А символ \n так и останется «торчать» во входном потоке. Возможное решение этой проблемы - удалить символ \n из входного потока путем вызова метода get без параметров, то есть после операторов 1 и 3 нужно вставить вызов cin. get () .

Однако есть и более простое решение - использовать в таких случаях метод getline, который после прочтения строки не оставляет во входном потоке символ \n.

Если в программе требуется ввести несколько строк, метод getline удобно использовать в заголовке цикла, например:

#include<iostream.h>

int main()

{

const int n = 80;

char s[n];

while(cin.getline(s. n))

{

cout << s << endl;

-             // обработка строки                                                      

}

return 0;

}

Рассмотрим теперь способы ввода-вывода строк, перекочевавшие в C++ из языка С. Во-первых, можно использовать для ввода строки известную нам функцию scanf, а для вывода - printf, задав спецификацию формата %s:

#include<stdio.h>

int main()

{

const int n = 10;

char s[n];

scan ("%s", s);

printf("%s",s);

return 0;

}

Имя строки, как и любого массива, является указателем на его начало, поэтому использовавшаяся в предыдущих примерах применения функции scanf операция взятия адреса (&) опущена. Ввод будет выполняться так же, как и для классов ввода-вывода - до первого пробельного символа. Чтобы ввести строку, состоящую из нескольких слов, используется спецификация %s (символы) с указанием максимального количества вводимых символов, например:

scanf("%10s", s);

Количество символов может быть только целой константой. При выводе можно задать перед спецификацией & количество позиций, отводимых под строку:

printf ("%15s". s);

Строка при этом выравнивается по правому краю отведенного поля. Если заданное количество позиций недостаточно для размещения строки, оно игнорируется, и строка выводится целиком.

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

#include <stdio.h>

int main()

{

const int n = 10;

char s[n];

gets(s);

puts(s);

return 0;

}

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

Функция puts (s) выводит строку s на стандартное устройство вывода, заменяя завершающий 0 символом новой строки. Возвращает неотрицательное значение при успехе или EOF при ошибке.

Функциями семейства printf удобнее пользоваться в том случае, если в одном операторе требуется ввести или вывести данные различных типов. Если же работа выполняется только со строками, проще применять специальные функции для ввода-вывода строк gets и puts.

Операции со строками. Для строк не определена операция присваивания, поскольку строка является не основным типом данных, а массивом. Присваивание выполняется с помощью функций стандартной библиотеки или посимвольно «вручную» (что менее предпочтительно, так как чревато ошибками). Например, чтобы присвоить строке р строку а, можно воспользоваться функциями strcpy или strncpy:

char a[100] = "Never trouble trouble";

char *p = new char[m];

strcry (p, a);

strcry (p, a, strlen(a) + 1);

Для использования этих функций к программе следует подключить заголовочный файл <string. h>.

Функция strcpy (р. а) копирует все символы строки, указанной вторым параметром (а), включая завершающий 0, в строку, указанную первым параметром (р).

Функция strncpy (p, а. n) выполняет то же самое, но не более n символов, то есть числа символов, указанного третьим параметром. Если нуль-символ в исходной строке встретится раньше, копирование прекращается, а оставшиеся до n символы строки р заполняются нуль-символами. В противном случае (если n меньше или равно длине строки а) завершающий нуль-символ в р не добавляется.

Обе эти функции возвращают указатель на результирующую строку. Если области памяти, занимаемые строкой-назначением и строкой-источником, перекрываются, поведение программы не определено.

Функция strlen(a) возвращает фактическую длину строки а, не включая нуль-символ.

Программист должен сам заботиться о том, чтобы в строке-приемнике хватило места для строки-источника (в данном случае при выделении памяти значение переменной m должно быть больше или равно 100), и о том, чтобы строка всегда имела завершающий нуль-символ.

Выход за границы строки и отсутствие нуль-символа являются распространенными причинами ошибок в программах обработки строк.

Для преобразования строки в целое число используется функция atoi(stг). Функция преобразует строку, содержащую символьное представление целого числа, в соответствующее целое число. Признаком конца числа служит первый символ, который не может быть интерпретирован как принадлежащий числу. Если преобразование не удалось, возвращает 0.

Аналогичные функции преобразования строки в длинное целое число (long) и в вещественное число с двойной точностью (double) называются atoi и atof соответственно.

Пример применения функций преобразования:

char a[ ] = "10) Рост-162см, вес-59.5кг";

int num;

long height;

double weight;

num = atoi(a);

height = atol(&a[11]);

weight = atof(&a[25]);

cout << num << '' << height << ''<< weight;

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

Работа с символами. Для хранения отдельных символов используются переменные типа char. Их ввод-вывод также может выполняться как с помощью классов ввода-вывода, так и с помощью функций библиотеки.

При использовании классов ввод-вывод осуществляется как с помощью операций помещения в поток « и извлечения из потока », так и методов get() и get (char). Ниже приведен пример применения операций:

#include <iostream.h>

int main()

{

char c, d, e;

cin >> c;

cin >> d >> e;

cout << c << d << e << endl;

return 0;

}

Вводимые символы могут разделяться или не разделяться пробельными символами, поэтому таким способом ввести символ пробела нельзя. Для ввода любого символа, включая пробельные, можно воспользоваться методами get() или get (с):

#include <iostream.h>

int main()

{

char c, d, e;

c = cin.get(); cin.get(d); cin.get(e);

cout << c << d << e << endl;

return 0;

}

Метод get () возвращает код извлеченного из потока символа, а метод get (с) записывает извлеченный символ в переменную, переданную ему в качестве аргумента, и возвращает ссылку на поток.

В заголовочном файле <stdiо. h> определена функция getchar() для ввода символов  со стандартного ввода, а также putchar() для вывода:

#include <iostream.h>

int main()

{

char c, d;

c = getchar();

putchar(c);

d = getchar();

putchar(d);

return 0;

}

В библиотеке также определен целый ряд функций, проверяющих принадлежность символа какому-либо множеству, например множеству букв (isalfa), разделителей (isspace), знаков пунктуации (ispunct), цифр (isdigit) и т. д.

Пример 8.1. Поиск подстроки

Написать программу, которая определяет, встречается ли в заданном текстовом файле заданная последовательность символов. Длина строки текста не превышает 80 символов, текст не содержит переносов слов, последовательность не содержит пробельных символов.

I. Исходные данные и результаты

Исходные данные:

1.  Текстовый файл неизвестного размера, состоящий из строк длиной не более 80 символов. Поскольку по условию переносы отсутствуют, можно ограничитъся поиском заданной последовательности в каждой строке отдельно. Следовательно, необходимо помнить только одну текущую строку файла. Для ее хранения выделим строковую переменную длиной 81 символ (дополнительный символ требуется для завершающего нуля).

2. Последовательность символов для поиска, вводимая с клавиатуры. Поскольку по условию задачи она не содержит пробельных символов, ее длина также не должна быть более 80 символов, иначе поиск завершится неудачей. Для ее хранения также выделим строковую переменную длиной 81 символ.

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

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

II. Алгоритм решения задачи

  1.  Построчно считывать текст из файла.

Для каждой строки проверять, содержится ли в ней заданная последовательность.

Если да, напечатать сообщение о наличии заданной последовательности и завершить программу.

При нормальном выходе из цикла напечатать сообщение об отсутствии заданной последовательности и завершить программу.

III. Программа и тестовые примеры 

Листинг 8.1

#include <iostream.h>

#include <string.h>

int main()

{

const int len = 81;                    //1                       

char word[len], line[len];             //2                    

setlocale( LC_ALL, "Russian" );

cout <<"Введите слово для поиска:"; cin >> word;

ifstream fin("text.txt",ios::in|ios::nocreate); //3       

if (!fin)

{

 cout <<"Ошибка открытия файла." << endl;

 return 1;                         //4

}                                                            

while (fin.getline(line, len))         //5

{                                 

 if (strstr(line, word))           //6

 {                                         

  cout <<"Отсутствует!" <<endl;

 }

}

cout << "Отсутствует!" << endl;

return 0;                                 //7                    

}

Рассмотрим помеченные операторы. В операторе 1 описывается константа, определяющая длину строки файла и длину последовательности. В операторе 2 описывается переменная 1ine для размещения очередной строки файла и переменная «ord для размещения искомой последовательности символов.

В операторе 3 определяется объект fin класса входных потоков if stream. С этим бъектом можно работать так же, как со стандартными объектами cin и cout, то есть использовать операции помещения в поток « и извлечения из потока ».

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

ifstream fin("с:\\prim\\cpp\\text.txt", ios::in |  ios::nocreate);                        // 3

В операторе 4 проверяется успешность создания объекта fin. Файлы, открываемые для чтения, проверять нужно обязательно! В операторе 5 организуется цикл чтения из файла в переменную. line.

Метод get line, описанный выше, при достижении конца файла вернет значение, завершающее цикл.

Для анализа строки в операторе 6 применяется функция strstrd (line, word). Она выполняет поиск подстроки word в строке line. Обе строки должны завершаться нуль-символами. В случае успешного поиска функция возвращает указатель на найденную подстроку, в случае неудачи - NULL. Если вторым параметром передается указатель на строку нулевой длины, функция возвращает указатель на начало строки line.

В качестве тестового примера приготовьте текстовый файл, состоящий из нескольких строк. Длина хотя бы одной из строк должна быть равна 80 символам. Для тестирования программы следует запустить ее по крайней мере два раза: введя с клавиатуры слово, содержащееся в файле, и слово, которого в нем нет.

Даже такую простую программу мы рекомендуем вводить и отлаживать по шагам. Предлагаемая последовательность отладки:

  1.  Ввести «скелет» программы (директивы #include, функцию main(), операторы1-4). Добавить контрольный вывод введенного слова. Запустив программу, проверить ввод слова и успешность открытия файла. Выполнить программу, задав имя несуществующего файла, для проверки вывода сообщения об ошибке. Удалить контрольный вывод слова.
  2.  Проверить цикл чтения из файла: добавить оператор 5 с его завершающей фигурной скобкой, внутри цикла поставить контрольный вывод прочитанной строки:

cout « line « endl;

Удалить контрольный вывод строки.

3. Дополнить программу операторами проверки и вывода сообщений. Для полной проверки программы следует выполнить ее для нескольких последовательностей. Длина одной из них должна составлять максимально допустимую - 80 символов.

Пример 8.2. Подсчет количества вхождений слова в текст

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

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

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

слово =

(начало строки | знак пунктуации | разделитель) символы, составляющие слово (конец строки | знак пунктуации | разделитель)

I. Исходные данные и результаты

Исходные данные:

  1.  Текстовый файл неизвестного размера, состоящий из строк длиной не более 80 символов. Поскольку по условию переносы отсутствуют, можно ограничиться поиском слова в каждой строке отдельно. Для ее хранения выделим строку длиной 81 символ.
  2.  Слово для поиска, вводимое с клавиатуры. Для его хранения также выделим строку длиной 81 символ.

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

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

II. Алгоритм решения задачи

  1.  Построчно считывать текст из файла.
  2.  Просматривая каждую строку, искать в ней заданное слово. При каждом
    нахождении слова увеличивать счетчик.

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

При обнаружении совпадения с символами, составляющими слово, требуется определить, является ли оно отдельным словом, а не частью другого. Например, мы задали слово «кот». Эта последовательность символов содержится, например, в словах «котенок», «трикотаж», «трескотня» и «апперкот». Следовательно, требуется проверить символ, стоящий после слова, а в случае, когда слово не находится в начале строки - еще и символ перед словом. Эти символы проверяются на принадлежность множеству знаков пунктуации и разделителей.

III. Программа и тестовые примеры

Разобьем написание программы на последовательность шагов.

Шаг 1. Ввести «скелет» программы (директивы #include, функцию main(), описание переменных, открытие файла). Добавить контрольный вывод введенного слова. Запустив программу, проверить ввод слова и успешность открытия файла. Для проверки вывода сообщения об ошибке следует выполнить программу еще раз, задав имя несуществующего файла.

#include <fstream.h>

int main()

{

const int len = 81;                          

char word[len], line[len];                 

setlocale( LC_ALL, "Russian" );

cout << "Введите слово для поиска:";

cin >> word;

ifstream fin("text.txt",ios::in|ios::nocreate);  

if (!fin)

{

cout << "Ошибка открытия файла." << endl;

}                                         

return 0;                                        

}

Шаг 2. Добавить в программу цикл чтения из файла, внутри цикла поставить контрольный вывод считанной строки (добавляемые операторы помечены признаком комментария):

#include <fstream.h>

int main()

{

const int len = 81;                          

char word[len], line[len];                 

setlocale( LC_ALL, "Russian" );

cout << "Введите слово для поиска:";

cin >> word;

ifstream fin("text.txt",ios::in|ios::nocreate);  

if (!fin)

{

cout << "Ошибка открытия файла." <<endl;

return 1;

}                                         

while (fin.getline(line,len))

{

cout << line << endl;

}

return 0;                                        

}

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

#include <fstream.h>

#include <string.h>

int main()

{

const int len = 81;                          

char word[len], line[len];                 

setlocale( LC_ALL, "Russian" );

cout << "Введите слово для поиска:";

cin >> word;

int l word = strlen(word);

ifstream fin("text.txt",ios::in|ios::nocreate);  

if (!fin)

{

cout << "Ошибка открытия файла." <<endl;

return 1;

}   

int count = 0;                                      

while (fin.getline(line,len))

{

char *p = line;

while (p = strstr(p,word))

{

cout << "Совпадение:" << p << endl;

p += l word; count++;

}

}

cout << count << endl;

return 0;                                        

}

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

Шаг 4. Добавить в программу анализ принадлежности символов, находящихся перед словом и после него, множеству знаков пунктуации и разделителей:

Листинг 8.2

#include <fstream.h>

#include <string.h>

#include <ctype.h>

int main()

{

const int len = 81;                          

char word[len], line[len];                 

setlocale( LC_ALL, "Russian" );

cout << "Введите слово для поиска:";

cin >> word;

int l word = strlen(word);

ifstream fin("text.txt",ios::in|ios::nocreate);  

if (!fin)

{

cout << "Ошибка открытия файла." <<endl;

return 1;

}   

int count = 0;                                      

while (fin.getline(line,len))

{

char *p = line;

while (p = strstr(p,word))

{

char *c = p;

p += l word; //слово не в начале строки?

if(c!=line) //символ перед словом не разделитель?

if(!ispunct(*(c-1))&&!isspase(*(c-1) )) continue;

//символ после слова разделитель?

if(ispunct(*p)||isspase(*p)||(*p=='\0') ) count++;

}

}

cout << "Количество вхождений слова:" << count << endl;

return 0;                                        

}

Здесь вводится служебная переменная с для хранения адреса начала вхождения подстроки. Символы, ограничивающие слово, проверяются с помощью функций ispunct и isspace, прототипы которых хранятся в заголовочном файле <ctype.h>. Символ, стоящий после слова, проверяется также на признак конца строки (для случая, когда искомое слово находится в конце строки).

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

  •  в начале строки;
  •  в конце строки;
  •  в середине строки;
  •  несколько раз в одной строке;
  •  как часть других слов, находящаяся в начале, середине и конце этих слов;
  •  в скобках, кавычках и других разделителях.

Длина хотя бы одной из строк должна быть равна 80 символам. Для тестирования программы следует выполнить ее по крайней мере два раза: введя с клавиатуры слово, содержащееся в файле, и слово, которого в нем нет.,

Теперь рассмотрим другой вариант решения этой задачи. В библиотеке есть функция strtok, которая разбивает переданную ей строку на лексемы в соответствии с заданным набором разделителей. Если мы воспользуемся этой функцией, нам не придется «вручную» выделять и проверять начало и конец слова, потребуется лишь сравнить с искомым словом слово, выделенное с помощью strtok. Правда, список разделителей придется задать вручную.

Пример 8.3. Вывод вопросительных предложений

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

I. Исходные данные и результаты

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

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

П. Алгоритм решения задачи

  1.  Открыть файл.
  2.  Определить его длину в байтах.
  3.  Выделить в динамической памяти буфер соответствующего размера.
  4.  Считать файл с диска в буфер.
  5.  Анализируя буфер посимвольно, выделять предложения. Если предложение оканчивается вопросительным знаком, вывести его на экран.

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

III. Программа и тестовые примеры

Ниже приводится текст программы. Рекомендуем вам самостоятельно разбить его для отладки на последовательность шагов аналогично предыдущим примерам, вставляя и удаляя отладочную печать. Файл с тестовым примером должен содержать предложения различной длины (от нескольких символов до нескольких строк), в том числе и вопросительные.

Листинг 8.3

#include <fstream.h>

#include <stdio.h>

int main()

{

 setlocale( LC_ALL, "Russian" );

ifstream fin("text.txt",ios::in|ios::nocreate);  

if (!fin)

{

cout << "Ошибка открытия файла." << endl;

return 1;

}   

fin.seekg(0,ios::end);             //1

long len = fin.tellg();            //2

char *buf = new char[len + 1];     //3

fin.seekg(0, ios :: beg);          //4

fin.read(buf. len);                //5

buf[len] = '\0';

long n = 0, i = 0, j = 0;          //6

while(buf[i])                      //7

{                     

  if(buf[i] = '?')                //8

{               

for(j = n; j <= i; j++) cout<< buf[j];

n = i + j;

}

if(buf[i] == '.'||buf[i] == '!') n = i + j;

i++;

}

fin.close();                       //9

cout << endl;

return 0;                                        

}

Для определения длины файла используются методы seekg и tel I g класса stream. С любым файлом при его открытии связывается так называемая текущая позиция чтения или записи. Когда файл открывается для чтения, эта позиция устанавливается на начало файла. Для определения длины файла мы перемещаем ее на конец файла с помощью метода seekg (оператор 1), а затем с помощью tel I g получаем ее значение, запомнив его в переменной 1 en (оператор 2).

Метод seekg(offset, org) перемещает текущую позицию чтения из файла на offset байтов относительно org. Параметр org может принимать одно из трех значений:

ios:: beg - от начала файла; i os:: cur - от текущей позиции; i os:: end - от конца файла.

beg, cur и end являются константами, определенными в классе 1 os, предке 1 f stream, а символы :: означают операцию доступа к этому классу.

В операторе 3 выделяется 1 en + -1 байтов под символьную строку buf, в которой будет храниться текст из файла. Мы выделяем на один байт больше, чем длина файла, чтобы после считывания файла записать в этот байт нуль-символ.

Для чтения информации требуется снова переместить текущую позицию на начало файла (оператор 4). Собственно чтение выполняется в операторе 5 с помощью метода read(buf, I en), который считывает из файла Ten символов (или менее, если конец файла встретится раньше) в символьный массив buf.

В операторе 6 определяются служебные переменные. В переменной п будет храниться позиция начала текущего предложения, переменная i используется для просмотра массива, переменная j - для вывода предложения.

Цикл просмотра массива buf (оператор 7) завершается, когда встретился нуль-символ. Если очередным символом оказался вопросительный знак (оператор 8), выполняется вывод символов, начиная с позиции п до текущей, после чего в переменную п заносится позиция начала нового предложения.

Оператор 9 (закрытие потока) в данном случае не является обязательным, так как явный вызов cl ose () необходим только тогда, когда требуется закрыть поток раньше окончания действия его области видимости.

Если требуется вывести результаты выполнения программы не на экран, а в файл, в программе следует описать объект класса выходных потоков ofstream, а затем использовать его аналогично другим потоковым объектам, например:

ofstream fout("textout.txt");

if(!fout)

{

cout << "Ошибка открытия файла вывода" << endl;

return 1;

}

...

fout << buf[j];

требуется закрыть поток раньше окончания действия:

fout.closeO;

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

while((buf[i] = fin.get()) != EOF

{

...

i++;

}

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

Листинг 8.4

#include <stdio.h>

int main()

{

FILE *ffseek (finin;

fin = fopen("text.txt","r");

setlocale( LC_ALL, "Russian" );

if (!fin)

{

puts("Ошибка открытия файла");

return 1;

}

fseek(fin. 0. SEEK END);

long len = ftell(fin);

char *buf = new char[len + 1];

const int l block = 1024;

int num block = len/l block;

fseek(fin. 0. SEEK SET);

fread(buf. l block, num block + 1. fin);

buf[len] = '\0';

long n = 0, i = 0, j = 0;

while(buf[i])

{

if (buf[i] == '?')

{

for (j = n; j <= i; j++)

putchar(buf[j]);

n = i + j;

}

}

fclose(fin);

printf('\n");

return 0;         

}

В операторе 1 определяется указатель на описанную в заголовочном файле <stdio.h> структуру FILE. Указатель именно такого типа формирует функция открытия файла fopen. Ее вторым параметром задается режим открытия файла. В данном случае файл открывается для чтения (г).

Файл можно открыть в двоичном (Ь) или текстовом (t) режиме. Эти символы записывают во втором параметре, например, "rb" или "rt". Двоичный режим означает, что символы перевода строки и возврата каретки (0x13 и 0x10) обрабатываются точно так же, как и остальные. В текстовом режиме эти символы преобразуются в одиночный символ перевода строки. По умолчанию файлы открываются в текстовом режиме.

Для позиционирования указателя текущей позиции используется функция f seek с параметрами, аналогичными соответствующему методу потока (операторы 3 и 7). Константы, задающие точку отсчета смещения, описаны в заголовочном файле <stdiо. h> и имеют имена:

SEEK_SET - от начала файла; SEEK_CUR - от текущей позиции; SEEK_END - от конца файла.

Чтение из файла выполняется функцией fread(buf, size, num. file) блоками по size байт. Требуется также задать количество блоков num. В программе размер блока задан в переменной равным 1024, поскольку размер кластера кратен степени двойки. В общем случае чем более длинными блоками мы читаем информацию, тем быстрее будет выполнен ввод. Для того чтобы обеспечить чтение всего файла, к количеству блоков добавляется 1 для округления после деления.

Вывод на экран выполняется посимвольно с помощью функции putchar.

Если требуется с помощью функций библиотеки вывести результаты выполнения программы не на экран, а в файл, в программе следует описать указатель на структуру FILE, с помощью функции fopen открыть файл для записи (второй параметр функции - w), а затем использовать этот указатель в соответствующих функциях вывода, например:

FILE *fout;

fout = fopen(textout.txt". "w");

setlocale( LC_ALL, "Russian" );

if (Ifout)

{

puts("Ошибка открытия файла вывода");

return 1;

}

putc(buf[j], fout);     // или fputc(buf[j], fout); 

После окончания вывода файл закрывается с помощью функции fclose:

fclose(fout);

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

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

Пример 8.4. Вывод предложений, состоящих из заданного количества слов

Написать программу, которая считывает текст из файла и выводит на экран только предложения, состоящие из заданного количества строк. UML-диаграмма этого алгоритма приведена на рисунке 8.1.

 Рисунок 8.1 - UML-диаграмма деятельности для примера 8.4

Листинг 8.5

#include "stdafx.h"

#include <iostream>

#include <fstream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

   cout<<"\n"

       <<"Задание: Считать текст из файла. Вывести на экран только предложения, \n"

       <<"\tсостоящие из заданного количества слов\n"

       <<"----------------------------------------\n";

  

   ifstream fin("text.txt",ios::in);

   if (! fin)

{

cout<<"Ошибка открытия файла"<<endl;

return 1;

}

   int nword;

  

   cout<<"Введите искомое число слов в предложении: ";

   cin>>nword;

   fin.seekg(0,ios::end);

   int len=fin.tellg();

   char *buf=new char [len+1];

   fin.seekg(0,ios::beg);

   fin.read(buf,len);

   buf[len]='\0';

   int l_beg=0,i=0,n=0,j=0;

   bool exist;

   exist=false;

   while (buf[i])

   {

      if (buf[i]==' ') n++;

      if (buf[i]=='.')

      {

         n++;

         if (n==nword)

         {

             for(j=l_beg;j<=i;j++)

               cout<<buf[j];

          exist=true;

          cout<<endl;

         }

         l_beg=i+2;

         i=i+2;

         n=0;

      }

     i++;

   }

   if (!exist) cout<<"Таких предложений не найдено"<<endl;

   fin.close();

   return 0;

}

Рисунок 8.2 – Файл для входных данных

Пример 8.5. Вывод предложений, содержащих максимальное количество знаков пунктуации

Написать программу, которая считывает текст из файла и выводит на экран только предложения, содержащие максимальное количество знаков пунктуации. UML-диаграмма этого алгоритма приведена на рисунке 8.3.

Рисунок 8.3 - UML-диаграмма деятельности для примера 8.5

Листинг 8.6

#include "stdafx.h"

#include <iostream>

#include <fstream>

#include <locale>

using namespace std;

int main()

{   setlocale( LC_ALL, "Russian" );

  locale loc ("Russian_Russia");

  cout << "Задание:\nНаписать программу, которая считывает текст\n из файла и выводит на экран предложения, \n содержащие максимальное количество знаков пунктуации.\n";

  /*устанавливаем русскую локаль - чтобы писать русскими буквами =)*/

 

  //открываем файл для чтения

  ifstream FileInput("file.txt",ios::in);

  //если файл не обнаружен, то аварийно выходим

  if (!FileInput)

{

cout << "Ошибка! Файл не открыт!" << endl;

return 1;

}

  cout << "Текст,в котором осуществляется поиск предложения с max кол-вом знаков пунктуации\n";

  cout << "Чтение из файла выполняется функцией fread(buf, size, num, file) блоками по size байт!Требуется также задать количество блоков num.В программе размер блока задан в переменной равным 1024, поскольку размер кластера кратен степени двойки.В общем случае чем более длинными блоками мы читаем информацию, тем быстрее будет выполнен ввод.Для того, чтобы обеспечить чтение всего файла, к количеству блоков добавляется 1 для округления после деления.\n";

  //устанавливаем позицию курсора в конец файла

  FileInput.seekg(0,ios::end);

  /*узнаем длину файла методом tellg(), который сообщает о позиции курсора в файле (т.е. сколько байт прошел курсор)*/

  int LengthOfFile = FileInput.tellg();

  //создаем переменную для хранения содержимого файла

  char *str = new char [LengthOfFile+1];

  //устанавливаем позицию курсора в начало файла

  FileInput.seekg(0,ios::beg);

  /*считываем содержимое файла в переменную str на заданное число байт LengthOfFile*/

  FileInput.read(str,LengthOfFile);

  //добавляем символ конца строки в переменную str

  str[LengthOfFile]='\0';

  

  /*pos - текущая позиция в файле;

  counter - текущее кол-во пунктационных знаков;

  max - текущее максимальное кол-во пунктационных знаков;

  l - число символов в предложении;

  begin, end - начало и конец предложения с макс. кол-вом пункт. знаков*/

  int pos = 0,l = 0,end = 0,begin = 0;

  int counter = 0,max = 0;

  //поиск предложения c макс кол-вом пункт. знаков

  while (str[pos])

  {

     /*если текущий символ явл-ся пунктационным, то счетчик увеличиваем на 1*/

     if (ispunct(str[pos],loc)) counter++;

     //если достигнут конец предложения, то...

     if (str[pos]=='.' || str[pos]=='!' || str[pos]=='?')   

        /*...при условии максиммального кол-ва пунктационных символов вычисляем начало и конец данного предложения...*/

        if (counter>max)

{

           max = counter;

           counter = 0;

           /*здесь вычисляем начало предложения из текущего положения вычитаем кол-во прошедших символов*/

           begin = pos - l;

           end = pos;

           l = 0;

}

        /*...иначе обнуляем количество пунктационных символов и количество символов в пердложении*/

        else {counter = 0; l = 0;}

      l++;

      pos++;

  }

  l = begin;

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

  for(l; l < end + 1; l++)

      cout << str[l];

 

  return 0;

  }

Рисунок 8.4 – Файл для входных данных

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

Написать программу, которая считывает текст из файла и определяет, сколько в нем слов, состоящих из не более чем четырех букв. UML-диаграмма этого алгоритма приведена на рисунке 8.5.

Рисунок 8.5 - UML-диаграмма деятельности для примера 8.6

Листинг 8.7

// Лабораторная работа №_8

#include "stdafx.h"

#include <windows.h>

#include <fstream>

#include <string.h>

#include <ctype.h>

#include <iostream>

#include <conio.h>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

   

   setlocale( LC_ALL, "Russian" );

int n, s, ss, i, ii;

const int len = 100;                          

char word[len], line[len];    

    cout<<"\t\tЛабораторная работа №_8\n\n";

    cout<< "  Написать программу, которая считывает текст из файла и определяет, сколько  в нем слов, состоящих из не более чем четырех букв. \n" ;

    cout<<"\nНажмте клавишу для продолжения...\n\n" ;

getch();

int lword = static_cast<int>(strlen(word));

ifstream fin("text.txt");  

if (!fin)

{

 cout << "Error opening file." <<endl;

 return 1;

}   

s = 0;

ss = 0;

int count = 0;                                      

while (fin.getline(line,len))

{

 char *p = line;

 ii = strlen(p);

 fin.seekg(0,ios::beg);

 for (i = 0; i < ii; i++)

 {

  if (p[i]!='\x20') s++ ;

  if (p[i]=='\x20')

{

   if (s < 5)

{

ss++;

   }

s = 0;

}

 }

}

 

cout  << "\nВ файле содержатся слова, содержащие не более 4-х букв в количестве  " << ss << " шт.";

   

getch();

return 0;

}

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

1.Проработать примеры, приведенные в лабораторной работе.

2. Составить программу с использованием двумерных локальных массивов для решения задачи. Размерности локальных массивов задавать именованными константами, значения элементов массива - в списке инициализации. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №1. Вариант:

1. Написать программу, которая считывает из текстового файла три предложения и выводит их в обратном порядке.

2. Написать программу, которая считывает текст из файла и выводит на экран только предложения, содержащие введенное с клавиатуры слово.

3. Написать программу, которая считывает текст из файла и выводит на экран только строки, содержащие двузначные числа.

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

5. Написать программу, которая считывает текст из файла и выводит его на экран, меняя местами каждые два соседних слова.

6. Написать программу, которая считывает текст из файла и выводит на экран только предложения, не содержащие запятых.

7. Написать программу, которая считывает текст из файла и определяет, сколько в нем слов, состоящих из не менее чем семи букв.

8. Написать программу, которая считывает текст из файла и выводит на экран только цитаты, то есть предложения, заключенные в кавычки.

9. Написать программу, которая считывает английский текст из файла и выводит на экран слова текста, начинающиеся и оканчивающиеся на гласные буквы.

10. Написать программу, которая считывает текст из файла и выводит на экран только строки, не содержащие двузначных чисел.

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

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

13. Написать программу, которая считывает текст из файла и выводит его на экран, зааменив цифры от 0 до 9 на слова «ноль», «один», ..., «девять», начиная каждое предложение с новой строки.

14. Написать программу, которая считывает текст из файла, находит самое длинное слово и определяет, сколько раз оно встретилось в тексте.

15. Написать программу, которая считывает текст из файла и выводит на экран сначала вопросительные, а затем восклицательные предложения.

16. Написать программу, которая считывает текст из файла и выводит его на экран, после каждого предложения добавляя, сколько раз встретилось в нем введенное с клавиатуры слово.

17. Написать программу, которая считывает текст из файла и выводит на экран все его предложения в обратном порядке.

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

19. Написать программу, которая считывает текст из файла и выводит на экран предложения, содержащие минимальное количество знаков пунктуации.

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

1. Названия лабораторной работы.

2. Цели и содержания лабораторной работы.

3. Ответов на контрольные вопросы лабораторной работы.

4. Формулировки индивидуальных заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

1. Характеристика видов строк в языке С++.

2. Как описываются строки?

3. Охарактеризуйте способы ввода-вывода строк.

4. Что делают функции gets () и puts ()?

5. Каким образом выполняется операция присваивания при работе со строками?

6. Какие функции предоставляет библиотека <string. h>? Какие действия они выполняют?

7. Охарактеризуйте способы ввода-вывода символов.

8. Какая функция используетя для многократного поиска вхождения подстроки?

9. Для чего нужна функция strtok?

10. Какие значения может принимать параметр org?

11. Какие действия необходимо выполнить, чтобы вывести результаты выполнения программы не на экран, а в файл?

Пример выполнения лабораторной работы №8: 

       1. Индивидуальное задание №1:

1.1. Постановка задачи:

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

1.2. UML-диаграмма:

1.3. Листинг программы:

// Лабораторная работа №8

// Индивидуальное задание №1

#include "stdafx.h"

#include <iostream>

#include <fstream>

#include "stdio.h"

#include "stdlib.h"

#include "conio.h"

#include "string.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

cout << "Лабораторная работа № 8\n"

        << "\nГорошко А.А., БАС-051\n"

        << "\nВариант № 6\n"

        << "\n\nИндивидуальное задание № 1:\n"

        << "\nНаписать программу, которая считывает текст из файла и\n"

        << "\nвыводит на экран только предложения, содержащие запятые.\n"

        << "\n\nРабота программы:\n";

 

int n, i;

bool flag = false;

char ch;

string str = "";

ifstream input("text.txt");

if (! input)

{

 cout << "Ошибка открытия файла" << endl;

 return 1;

}

input.seekg(0,ios::end);             

long unsigned int len = input.tellg();

input.seekg(0, ios :: beg);          

for (i = 0; i < len; i++)

{

 ch = input.get();

 if (ch==',') flag = true;

 if (ch=='.')  

 {

  if (flag) cout << str.c_str();

  str = "";

  flag = false;

 }

 else

  str+=ch;

};

 

getch();

return 0;

} 

1.4. Результаты работы программы:

Лабораторная работа №9.
Структуры в языке C++

Цель работы и содержание: закрепление знаний о структурах, составление программ со структурами.

Ход работы

Основные сведения о структурах в языке С++. Структуры в C++ обладают практически теми же возможностями, что и классы, но чаще их применяют просто для логического объединения связанных между собой данных. В структуру, в противоположность массиву, можно объединять данные различных типов.

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

struct Event 

{

int hour, min;

char theme[100], name[100];

int num;

}

Имя этого типа данных - Event. Можно описать переменные этого типа точно так же, как переменные встроенных типов, например:

Event el. e2[10];    // структура и массив структур

Если структура используется только в одном месте программы, можно совместить описание типа с описанием переменных, при этом имя типа можно не указывать:

struct

{

int hour, min;

char theme[100]. name[100];

int num;

}

el, e2[10];

Переменные структурного типа можно размещать и в динамической области памяти, для этого надо описать указатель на структуру и выделить под нее место:

Event *pe = new Event;     //структура

Event *pm = new Event[m];  //массив стуктур

Элементы структуры называются полями. Поля могут быть любого основного типа, массивом, указателем, объединением или структурой. Для обращения к полю используется операция выбора («точка» для переменной и -> для указателя), например:

el.hour = 12; el.min = 30;

strncpy(e2[0]).theme. "Выращивание кактусов в условиях Крайнего Севера". 99);

pe->num = 30;      // или (*pe).num = 30;

pm[2].hour = 14;   // или (*(pm + 2)).hour = 14;

Структуры одного типа можно присваивать друг другу:

*ре = el: pm[l] » el: pm[4] = e2[0];

Но присваивание - это и все, что можно делать со структурами целиком. Другие операции, например сравнение на равенство или вывод, не определены. Впрочем, пользователь может задать их самостоятельно, поскольку структура является видом класса, а в классах можно определять собственные операции.

Ввод/вывод структур, как и массивов, выполняется поэлементно. Вот, например, как выглядит ввод и вывод описанной выше структуры el с использованием классов ввода-вывода (<iostream.h>):

cin » el.hour » el.min;

cin.getline(el.theme. 100);

cout « el.hour «' '« el.min «' '« el.theme « endl;

Вариант с использованием ввода-вывода в стиле С (подключается заголовочный файл <stdio.h>):

scanfC'MM", Sel.hour, Sel.min); gets (el. theme);

printf("&d %d %s". el.hour, el.min. el.theme);

Структуры (но, конечно, не динамические) можно инициализировать перечислением значений их элементов:

Event еЗ - {12. 30. "Выращивание кактусов в условиях Крайнего Севера", 25};

Пример 9.1. Поиск в массиве структур

В текстовом файле хранится база отдела кадров предприятия. На предприятии 100 сотрудников. Каждая строка файла содержит запись об одном сотруднике. Формат записи: фамилия и инициалы (30 поз., фамилия должна начинаться с первой позиции), год рождения (5 поз.), оклад (10 поз.). Написать программу, которая по заданной фамилии выводит на экран сведения о сотруднике, подсчитывая средний оклад всех запрошенных сотрудников.

I. Исходные данные, результаты и промежуточные величины

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

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

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

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

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

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

II. Алгоритм решения задачи очевиден:

  1.  Ввести из файла в массив сведения о сотрудниках.
  2.  Организовать Цикл вывода сведений о сотруднике:

- ввести с клавиатуры фамилию;

- выполнить поиск сотрудника в массиве;

- увеличить суммарный оклад и счетчик количества сотрудников;

- вывести сведения о сотруднике или сообщение об их отсутствии;

3. Вывести средний оклад.

Необходимо решить, каким образом будет производиться выход из цикла вывода сведений о сотрудниках. Для простоты условимся, что для выхода из цикла вместо фамилии следует ввести слово end.

  1.  Программа и тестовые примеры

        Листинг 9.1

#include <fstream.h>

#include <string.h>

#include <stdlib.h>     //#include <windows.h> //0

int main()

const int l name = 30, l year = 5, l pay = 10,

         l buf = l name + l pay;              //1

struct Man                                     //2

{                                    

 int birth year;

 char name[l name + 1];

 float pay;

};

const int l dbase = 100;

Man dbase [l dbase];                           //3

char buf[l buf + 1];                           //4

char name[l name + 1];                    //5

ifstream fin("dbase.txt",                    //6 ios::in|ios::nocreate);

if (!fin)

{

cout << "Ошибка открытия файла";

return 1;

}

int i = 0;

while(fin.getline(buf.l buf))                   //7

{              

 if (i >= 1 dbase)

 {

cout << "Слишком длинный файл";

return 1;

     }

 strncpy(dbase[i].name.buf.l name);

 dbase[i].name[l name] = '\0';

 dbase[i].birth year = atoi(&buf[l name]);

 dbase[i].pay = atof(&buf[l name + l year]);

 i++;

}

int n record = i.n man = 0;                   //8

float mean pay = 0;

while(true)                                   //9

{                               

cout <<"Введите фамилию или слово end":cin >> name;

               //DemToChar(name, name);  //10

if(strcmp(name. "end") == 0)break;     //11

bool not found = true;           //12

for (i = 0; i < n record; i++)            //13

{           

if (strstr(dbase[i]. name. name))      //14

if (dbase[i]. name[strlen(name)] == '')//15

{      

strcpy(name. dbase[i]. name);

        // CharToOem(name.name); //16

cout << name << dbase[i].birth year << '' << dbase[i].pay << endl;

n man++; mean_pay += dbase[i].pay;

not found = false;

}

}

if (not found) cout<<"Такого сотрудника нет"<<endl;

}

if (n man > 0) cout << "Средний оклад:" << mean pay/ n man << endl;                               //17

 

return 0;

}

В операторе 1 заданы именованные константы, в которых хранится формат входного файла, то есть длина каждого из полей записи (строки файла). Такой подход позволяет при необходимости легко вносить в программу изменения. Длина буфера, в который будет считываться каждая строка файла, вычисляется как сумма длин указанных полей. В операторе 2 определяется структура Man для хранения сведений об одном сотруднике. Длина поля, в котором будет находиться фамилия, задана с учетом завершающего нуль-символа. В операторе 3 определяется массив структур dbase для хранения всей базы. Его размерность также задается именованной константой. В операторах 4 и 5 задаются промежуточные переменные: буфер buf для ввода строки из файла и строка name для фамилии запрашиваемого сотрудника. В операторе 6 выполняется открытие файла dbase. txt для чтения. Предполагается, что этот файл находится в том же каталоге, что и текст программы, иначе следует указать полный путь. Входной файл следует создать в любом текстовом редакторе до первого запуска программы в соответствии с форматом, заданным в условии задачи. Файл для целей тестирования должен состоять из нескольких строк, причем необходимо предусмотреть случай, когда одна фамилия является частью другой (например, Иванов и Ивановский). Не забудьте проверить, выдается ли диагностическое сообщение, если файл не найден.

Цикл 7 выполняет построчное считывание из файла в строку buf и заполнение очередного элемента массива dbase. Счетчик 1 хранит индекс первого свободного элемента массива. Для формирования полей структуры используются функции копирования строк strncpy, преобразования из строки в целое число atoi и преобразования из строки в вещественное число atof. Обратите внимание на то, что завершающий нуль-символ в поле фамилии заносится «вручную», поскольку функция strncpy делает это только в случае, если строка-источник короче строки-приемника. В функцию atoi передается адрес начала подстроки, в которой находится год рождения. При каждом проходе цикла выполняется проверка, не превышает ли считанное количество строк размерность массива. При тестировании программы в этот цикл следует добавить контрольный вывод на экран считанной строки, а также сформированных полей структуры. Для проверки выдачи диагностического сообщения следует временно задать константу ljjbase равной, а затем меньшей фактического количества строк в файле.

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

Кстати, можно записать эту проверку и так:

while(fin.getline(buf.l buf)&& i < l dbase)

{

-

i++;

}

if (i >= l dbase)

{

cout << "Слишком длинный файл";

return 1;

}

В операторе 8 определяются две переменные: n_record для хранения фактического количества записей о сотрудниках и njnan - для подсчета сотрудников, о которых будут выдаваться сведения. Следует также не забыть обнулить переменную mean pay, в которой в следующем цикле будет накапливаться сумма окладов.

Цикл поиска сотрудников по фамилии организован как бесконечный (оператор 9) с принудительным выходом (оператор 11). Некоторые специалисты считают, что такой способ является плохим стилем, и для выхода из цикла следует определить переменную-флаг, но нам кажется иначе.

В операторе 12 определяется переменная-флаг not_found для того, чтобы после окончания цикла поиска было известно, завершился ли он успешно. Имя переменной следует выбирать таким образом, чтобы по нему было ясно, какое значение является истинным:

if (not found) cout «"Такого сотрудника нет" «endl;

В операторе 13 организуется цикл просмотра массива структур (просматриваются только заполненные при вводе элементы). Проверка совпадения фамилии сотрудника производится в два этапа. В операторе 14 с помощью функции strstr поиска подстроки определяется, содержится ли в поле базы name искомая последовательность букв, а в операторе 15 проверяется, есть ли непосредственно после фамилии пробел (если пробела нет, то искомая фамилия является частью другой, и эта строка нам не подходит). Такая простая проверка возможна из-за условия задачи, по которому фамилия должна начинаться с первой позиции каждой строки.

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

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

Проверка переменной n man в операторе 17 необходима для того, чтобы в случае, если пользователь не введет ни одной фамилии, совпадающей с фамилией в базе, не выполнялось деление на 0.

Крупным недостатком нашей программы является то, что вводить фамилию сотрудника требуется именно в том регистре, в котором она присутствует в базе. Для преодоления этого недостатка необходимо перед сравнением фамилий переводить все символы в один регистр. Для символов латинского алфавита в библиотеке есть функции to!ower (с) и toupper (с), переводящие переданный им символ с в нижний и верхний регистр соответственно, аналогичные функции для символов русского алфавита придется написать самостоятельно.

Если в базе есть несколько сотрудников с одной и той же фамилией, программа выдаст сведения обо всех.

Теперь рассмотрим вариант записи этой же программы с помощью библиотечных функций ввода-вывода:

Листинг 9.2

#include <stdio.h>

#include <string.h>     //#include <windows.h>  //0

int main()

const int l name = 30;                          //1

struct Man

{

 int birth year;

 char name[l name + 1];

 float pay;

};

const int l dbase = 100;

Man dbase[l dbase];

char name[l name + 1];

FILE *fin;

if ((fin = fopen("dbase.txt", "r"))== NULL)

{

puts("Ошибка открытия файла\n");

return 1;

}

int i = 0;

while(!feof(fin))

{

fgets(dbase[i].name.l name.fin);

fscanf(fin. "%l%f\n" .&dbase[i].birth year. &dbase[i].pay);                                 //2

i++;

 if (i > 1 dbase)

{

puts("Слишком длинный файл\n");

return 1;

}

}

int n record = i, n man = 0;

float mean pay = 0;

while(true)

{

puts("Введите фамилию или нажмите Enter для окончания:");

gets(name);

if(strlen(name) == 0)break;                 //3

              //OemToChar(name, name);     //4

bool not found = true;

for (i = 0; i < n record; i++)

{

if (strstr(dbase[i]. name. name))

if (dbase[i]. name[strlen(name)] == '')

{

strcpy(name. dbase[i]. name);

        // CharToOem(name.name); //5

printf("%30s%5i%10.2f\n".name. dbase[i].birth year.

dbase[i].pay);               //6

n man++; mean_pay += dbase[i].pay;

not found = false;

}

}

if (not found) puts("Такого сотрудника нет\n");

}

if(n man > 0) printf("Средний оклад: %10.2f\n".mean pay/n man};

return 0;

}

Из всех именованных констант осталась одна, задающая длину поля фамилии (l name, оператор 1). Все остальные константы определять нет смысла, потому что ввод осуществляется не через буферную переменную, а непосредственно в поля структуры с помощью функции чтения строки fgets и форматного ввода fscanf (оператор 2). Эта функция сама выполняет действия по преобразованию подстроки в число, которые мы явным образом задавали в предыдущей программе.

Мы упростили выход из цикла ввода запросов, теперь для завершения цикла достаточно нажать клавишу Enter (оператор 3). Для вывода сведений о сотруднике мы использовали функцию pri ntf (оператор 6).

Пример 9.2. Сортировка массива структур

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

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

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

Листинг 9.3

#include <stdlib.h>

#include <string.h>

#include <fstream.h>  

int main()

const int l name = 30,l yea r = 5, l pay = 10, l buf = l name + l pay;

struct Man

{

int birth year;

char name[l name + 1];

float pay;

};

const int l dbase = 100;

Man dbase[l dbase];

char buf[l buf + 1];

ifstream fin("dbase.txt", ios::in|ios::nocreate);

if (!fin)

{

cout << "Ошибка открытия файла";

return 1;

}

int i = 0;

while(fin.getline(buf.l_buf))

{

strncpy(dbase[i].name.buf.l name);

dbase[i].name[l name] = '\0';

dbase[i].birth year = atoi(&buf[l name]);

dbase[i].pay = atof(&buf[l name + l year]);

i++;

if(i > l dbase)

{

cout <<"Слишком длинный файл"<<endl;

return 1;

}

}

int n record = i;  

fin.close();

ofstream fout("dbase.txt");

if(!fout){cout <<"Ошибка открытия файла" << endl; return 1;

for (i = 0; i < n record - l; i++)

{

//Принимаем за наименьший первый из рассматриваемых элементов

int imin = i;

//поиск номера минимального элемента из неупорядоченных

 

for(int j = i + 1; j < n record; j++)

if (dbase[j].birth year < dbase[imin].birth year)imin = j;

//обмен двух элементов массива структур

Man a = dbase[i];

dbase[i] = dbase[imin];

dbase[imin] = a;

}

for(i = 0; i < n record; i++)

{

fout << dbase[i].name << dbase[i].birth year <<''<< dbase[i].pay << endl;

}

fout.close();

cout << "Сортировка завершена" << endl;

return 0;

}

Элементами массива в данном примере являются структуры. Для структур одного типа определена операция присваивания, поэтому обмен двух Элементов массива структур выглядит точно так же, как для основных типов данных.

Для того чтобы записать результаты в файл с тем же именем, файл, открытый для чтения, закрывается, а затем открывается файл с тем же именем для записи (говоря более строго, создается объект выходного потока ostream с именем fout). При этом старый файл на диске уничтожается и создается новый, пустой файл, в который и производится запись массива.

Пример 9.3. Структуры и бинарные файлы

Написать две программы. Первая считывает информацию из файла, формат которого описан в примере 8.1, и записывает ее в бинарный файл. Количество записей в файле не ограничено. Вторая программа по номеру записи корректирует оклад сотрудника в этом файле.

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

Бинарный файл открывается в двоичном режиме, а чтение/запись в него выполняются с помощью функций библиотеки fread и fwrite.

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

Листинг 9.4

//Создание бинарного файла из текстового

#include <stdio.h>

#include <string.h>

int main()

const int l name = 30;

struct

{

   char name[l name + 1];

int birth year;

  float pay;

}

man;

FILE *fin;

if ((fin = fopen("dbase.txt", "r")) == NULL)

{

puts("Ошибка открытия вх. файла\n");

return 1;

}

FILE *fout;

if ((fout = fopen("dbase.bin", "wb")) == NULL)

{

puts("Ошибка открытия вых. файла\n");

return 1;

}

while  (!feof(fin))

{

fgets(man.name.l name.fin);

fscanf("%s%5i%10.2f\n", man.name. man.birth year. man.pay);             //отладочная печать

fwrjte(&man. sizeof(man). l. fout);

}

fclose(fout);

printf("Бинарный файл написан\n");

return 0;

}

Для формирования записей в бинарном файле здесь применяется функция fwri te:

size t fwrite(const void *p. size t size, size t n, FILE *f)

Она записывает n элементов длиной size байт из буфера, заданного указателем р, в поток f. Возвращает число записанных элементов.

Для чтения из бинарного файла во второй программе будем применять функцию fread:

sizejt fread(void *p.size t size,size t n,FILE *f);

Она считывает n элементов длиной size байт в буфер, заданный указателем р, из потока f. Возвращает число считанных элементов, которое может быть меньше, чем запрошенное.

Листинг 9.5

//Корректировка бинарного файла

#include <stdio.h>

#include <string.h>

int main()

const int l name = 30;

struct

{

   char name[l name + 1];

int birth year;

  float pay;

}

man;

FILE *fout;

if ((fout = fopen("dbase.bin", "wb"))== NULL)  //1

{  

puts("Ошибка открытия  файла\n");

return 1;

}

fseek(fout.0.SEEK END);

int n record = ftell(fout)/sizeof(man);        //2

int num;

while(true)                                    //3

{                                  

puts("Введите номер записи или -1:");

scanf("%i".&num);

if(num < 0||num >= n record)break;

fseek(fout. num * sizeof(man). SEEK SET);

fread(&man. Sizeof(man). 1. fout);

printf("%s%5i&10.2f\n". man.name. man. birth year. man.pay);

puts("Введите новый оклад:");

scanf("%f". &man.pay);

fseek(fout. num*sizeof(man).SEEK SET);

fwrite(&man. sizeof(man). 1. fout);

printf("%s%5i&10.2f\n". man.name. man. birth year. man.pay);

}

fclose(fout);

printf("Корректировка завершена\n");

return 0;

}

В операторе 1 открывается сформированный в предыдущей задаче бинарный файл. Обратите внимание на режим открытия: г обозначает возможность чтения и записи, b  двоичный режим. Чтобы проконтролировать введенный номер записи, в переменную n record заносится длина файла в записях (оператор 2). До этого указатель текущей позиции файла устанавливается на его конец с помощью функции f seek, следовательно, в результате вызова ftel1 будет получен размер файла в байтах.

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

Пример 9.4. Структуры в динамической памяти

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

Листинг 9.6

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

const int l name = 30;

struct Man

{

   char name[l name + 1];

int birth year;

  float pay;

}

int compare(const void *man1. const void *man2);//1

int main()

{

FILE *fbin;

if ((fbin = fopen("dbase.bin", "rb")) == NULL)

{   

puts("Ошибка открытия  файла\n");

return 1;

}

fseek(fbin.0.SEEK_END);

int n record = ftell(fbin)/sizeof(Man);   

Man *man=new Man[n record];                    //2

fseek(fout. num*sizeof(man). SEEK SET);        //3

fread(man. Sizeof(Man). n record. fbin);       //4

fclose(fbin);

qsort(man. n record. sizeof(Man). compare);    //5

for (int i = 0; i < n record; i++)

printf("%s%5i&10.2f\n". man[i].name. man[i]. birth year. man.pay);

return 0;

}

int compare(const void *man1. const void *man2)

{

return stromp(((Man*) man1) -> name. ((Man*)man2) -> name);

}

Рассмотрим моменты, которые отличают эту программу от предыдущих. Во-первых, это чтение из бинарного файла. После открытия файла мы, как и в предыдущей программе, заносим в переменную n record его размер в записях, а затем выделяем в динамической памяти место под весь массив структур (оператор 2). Функция fread позволяет считать весь файл за одно обращение (оператор 4), после чего файл уже больше не требуется, поэтому лучше его сразу же закрыть.

Для сортировки мы в образовательных целях и для разнообразия использовали стандартную функцию qsort (оператор 5). Ее прототип находится в заголовочном файле <stdlib.h>. Функция может выполнять сортировку массивов любых размеров и типов. У нее четыре параметра:

  1.  указатель на начало области, в которой размещается упорядочиваемая информация;
  2.  количество сортируемых элементов;
  3.  размер каждого элемента в байтах;
  4.  имя функции, которая выполняет сравнение двух элементов.

Раз функция qsort универсальна, мы должны дать ей информацию, как сравнивать сортируемые элементы и какие выводы делать из сравнения. Значит, мы должны сами написать функцию, которая сравнивает два любых элемента, и передать ее в qsort. Имя функции может быть любым. Мы назвали ее compare. Оператор 1 представляет собой заголовок (прототип) функции, он необходим компилятору для проверки правильности ее вызова.

Для правильной работы qsort требуется, чтобы наша функция имела два параметра - указатели на сравниваемые элементы. Они должны иметь тип void. Функция должна возвращать значение, меньшее нуля, если первый элемент меньше второго, равное нулю, если они равны, и большее нуля, если первый элемент больше. При этом массив будет упорядочен по возрастанию. Если мы хотим упорядочить массив по убыванию, нужно изменить возвращаемые значения так: если первый элемент меньше второго, возвращать значение, большее нуля, а если больше - меньшее.

Внутри функции надо привести указатели на void к типу указателя на структуру Man. Для этого мы использовали операцию приведения типа в стиле С (Man *). Более грамотно применять для этого операцию reinterpret_cast, введенную в стандарт C++ относительно недавно. Старые компиляторы могут ее не поддерживать. Функция compare с использованием reinterpret_cast выглядит вот таким устрашающим образом:

int compare(const void *man1. const void *man2)

{

return stromp((reinterpret cast < const Man* > (man1)) -> name.(reinterpret cast < const Man* > (man2)) -> name);

}

Чтобы описание структуры было известно в функции compare, мы перенесли описание структуры, а вместе с ней и описание необходимой ей константы lname в глобальную область.

Для упорядочивания массива по другому полю надо изменить функцию сравнения. Вот, например, как она выглядит при сортировке по возрастанию года рождения:

int compare(const void *man1. const void *man2)

{

int p;

if(((Man*)man1) -> birth year < ((Man*)man2)-> birth year)p =- 1;

else if(((Man*)man1) -> birth year < ((Man*)man2)-> birth year) p = 0;

else p = 1;

return p;

}

Можно записать то же самое более компактно с помощью тернарной условной операции. Для разнообразия приведем функцию для сортировки по убыванию оклада:

int compare(const void *man1. const void *man2)

{

return ((Man*)man1)->pay==((Man*)man2)->pay?-1;

((Man*)man1)->pay == ((Man*)man2) -> pay?0;

1;

}

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

1.Проработать примеры, приведенные в лабораторной работе.

2. Составить программу с использованием структур. Номер варианта определяется по формуле , где  - номер студента по списку преподавателя.

Индивидуальное задание №1. Вариант:

1. Описать структуру с именем STUDENT, содержащую следующие поля: фамилия и инициалы; номер группы; успеваемость (массив из пяти элементов).

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

2. Описать структуру с именем STUDENT, содержащую следующие поля: фамилия и инициалы; номер группы; успеваемость (массив из пяти элементов).
        Написать программу, выполняющую следующие действия:
 ввод с клавиатуры данных в массив, состоящий из десяти структур типа STUDENT; записи должны быть упорядочены по возрастанию среднего балла; вывод на дисплей фамилий и номеров групп для всех студентов, имеющих оценки 4 и 5; если таких студентов нет, вывести соответствующее сообщение.

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

4. Описать структуру с именем AEROFLOT, содержащую следующие поля: название пункта назначения рейса; номер рейса; тип самолета.

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

5. Описать структуру с именем AEROFLOT, содержащую следующие поля: название пункта назначения рейса; номер рейса; тип самолета.

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

6. Описать структуру с именем TRAIN, содержащую следующие поля: название пункта назначения; номер поезда; время отправления.

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

7. Описать структуру с именем TRAIN, содержащую следующие поля: название пункта назначения; номер поезда; время отправления.

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

8. Описать структуру с именем TRAIN, содержащую следующие поля: название пункта назначения; номер поезда; время отправления.

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

9. Описать структуру с именем MARSH, содержащую следующие поля: название начального пункта маршрута; название конечного пункта маршрута; номер маршрута.

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

10. Описать структуру с именем MARSH, содержащую следующие поля: название начального пункта маршрута; название конечного пункта маршрута; номер маршрута.

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

11. Описать структуру с именем NOTE, содержащую следующие поля: фамилия, имя; номер телефона; дата рождения (массив из трех чисел).

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

12. Описать структуру с именем NOTE, содержащую следующие поля: фамилия, имя; номер телефона; дата рождения (массив из трех чисел).

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

13. Описать структуру с именем NOTE, содержащую следующие поля: фамилия, имя; номер телефона; дата рождения (массив из трех чисел).

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

14. Описать структуру с именем ZNAK, содержащую следующие поля: фамилия, имя; знак Зодиака; дата рождения (массив из трех чисел).

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

15. Описать структуру с именем ZNAK, содержащую следующие поля: фамилия, имя; знак Зодиака; дата рождения (массив из трех чисел).

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

16. Описать структуру с именем ZNAK, содержащую следующие поля: фамилия, имя; знак Зодиака; дата рождения (массив из трех чисел).

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

17. Описать структуру с именем PRICE, содержащую следующие поля: название товара; название магазина, в котором продается товар; стоимость товара в руб.

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

18. Описать структуру с именем PRICE, содержащую следующие поля: название товара; название магазина, в котором продается товар; стоимость товара в руб.

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

19. Описать структуру с именем ORDER, содержащую следующие поля: расчетный счет плательщика; расчетный счет получателя; перечисляемая сумма в руб.

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

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

1. Названия лабораторной работы.

2. Цели и содержания лабораторной работы.

3. Ответов на контрольные вопросы лабораторной работы.

4. Формулировки индивидуальных заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

1. Для чего применяют структуры?

2. Что представляют собой поля структуры?

3. Варианты ввода-вывода структур.

4. Какие функции могут использоваться для формирования полей структуры?

5. Что такое бинарные файлы? Для чего они применяются и каковы их основные преимущества?

6. Назовите параметры функции qsort. В каком заголовочном файле находится ее прототип?

Пример выполнения лабораторной работы №9:

1. Индивидуальное задание №1:

1.1. Постановка задачи:

Составить программу с использованием двумерных локальных массивов для решения задачи. Размерности локальных массивов задавать именованными константами, значения элементов массива — в списке инициализации.

Задача: описать структуру с именем WORKER, содержащую следующие поля: фамилия и инициалы работника; G - название занимаемой должности; год поступления на работу.

Написать программу, выполняющую следующие действия: 

- ввод с клавиатуры данных в массив, состоящий из десяти структур типа WORKER;

- записи должны быть размещены по алфавиту;  

- вывод на дисплей фамилий работников, чей стаж работы в организации превышает значение, введенное с клавиатуры; 

- если таких работников нет, вывести на дисплей соответствующее сообщение.

1.2. UML-диаграмма:

1.3. Листинг программы:

// Лабораторная работа №9

// Индивидуальное задание №1

#include "stdafx.h"

#include <iostream>

#include "conio.h"

#include "math.h"

#include "windows.h"

#include "stdio.h"

#include "stdlib.h"

#include "time.h"

#include <dos.h>

using namespace std;

struct WORKER

{

string fio;

string post;

int iyear;

};

int _tmain(int argc, _TCHAR* argv[])

{

setlocale( LC_ALL, "Russian" );

cout << "Лабораторная работа № 9\n"

         << "\nГорошко А.А., БАС-051\n"

         << "\nВариант № 6\n"

 << "\n\nИндивидуальное задание № 1:\n"

         << "\nОписать структуру с именем WORKER, содержащую следующие поля:\n"

         << "\nфамилия и инициалы работника;\n"

 << "\nназвание занимаемой должности;\n"

 << "\nгод поступления на работу.\n"

 << "\n\nНаписать программу, выполняющую следующие действия:\n"

  << "\nввод с клавиатуры данных в массив, состоящий из десяти структур типа WORKER;\n"

 << "\nзаписи должны быть размещены по алфавиту;\n"

 << "\nвывод на дисплей фамилий работников, чей стаж работы в организации\n"

 << "\nпревышает значение, введенное с клавиатуры;\n"

 << "\nесли таких работников нет, вывести на дисплей соответствующее сообщение.\n"

 << "\n\nРабота программы:\n";

 

SYSTEMTIME sm;

GetLocalTime(&sm);

setlocale(LC_ALL, ".1251");

string temp_str;

time_t *timeanddate;

int const n = 10;

int i, j, stag, current_year = sm.wYear, kol = 0;

char temp[255];

WORKER *works = new WORKER[n];

for (i = 0; i < n; i++)

{

 cout << "\nВведите данные " << i + 1 << "-го сотрудника:\n"

      << "Введите ФИО: ";

 cin >> temp;

 works[i].fio = temp;

 cout << "Введите должность: ";

 cin >> temp;

 works[i].post = temp;

 cout << "Введитие год поступления на работу: ";

 cin >> works[i].iyear;

}

for (i = 0; i < n; i++)

 for (j = 0; j < n-1; j++)

 {

  if (strcmp((const char*) works[j].fio.c_str(), (const char*) works[j + 1].fio.c_str()) > 0)

  {

   temp_str = works[j].fio;

   works[j].fio = works[j + 1].fio;

   works[j+1].fio = temp_str;

  }

 }

cout << "\n\nСортировка записей по алфавиту:\n ";

for (i = 0; i < n; i++)

{

 cout << '\n' << i + 1 << "-ый сотрудник:\n";

 cout << "ФИО: " << works[i].fio.c_str() << '\n';

 cout << "Должность: " << works[i].post.c_str() << '\n';

 cout << "год поступления на работу: " << works[i].iyear << '\n';

}

cout << "\n\nВведите стаж для отбора: ";

cin >> stag;

cout << "\nСотрудники, удовлетворяющие отбору:\n";

for (i = 0; i < n; i++)

{

 if (current_year-works[i].iyear >= stag)

 {

  kol++;

  if (kol!=0)

  cout << "\nФИО: " << works[i].fio.c_str() << '\n';

  cout << "Должность: " << works[i].post.c_str() << '\n';

  cout << "год поступления на работу: " << works[i].iyear << '\n';

 }

}

if (kol==0) cout << "\n\nНе найдены сотрудники, удовлетворяющие отбору!\n";

 

getch();

return 0;

}

2.4. Результаты работы программы:

Лабораторная работа №10.
Классы в языке C++

Цель работы и содержание: закрепление знаний о классах, составление программ с классами.

Ход работы

Структура программы на объектно-ориентированном языке. Структура программы на объектно-ориентированном языке состоит из трех пунктов:

  1.  В основе лежит базовый класс (класс – это абстрактный тип данных) – он самый простой;
  2.  Классы могут быть независимыми;
  3.  Строится иерархия наследования, связь классов, порождающиеся классы является более сложными.

В результате имеем следующую иерархию:

Базовый класс является простейшим из всех классов. Базовых классов может быть несколько и их можно добавлять в процессе эксплуатации. Сложность класса увеличивается с номером уровня. Внизу иерархии стоят самые сложные функции.

Свойства:

  •  Статика (обычно не меняется);
  •  Динамика (меняется).

Пример:

Дата – абстракция

Статика:

число,

месяц,

год.

+ класс

Динамика:

помнить следующую дату,

помнить предыдущую дату,

вычисление промежутков между датами.

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

- имеет какое-то состояние (или находится в каком-то состоянии). К примеру, про собаку можно сказать, что она имеет имя, окраску, возраст, голодна она или нет и т.д.

- имеет определенное поведение. Т.е., та же собака может вилять хвостом, есть, лаять, прыгать и т.д.

Объект в ООП состоит из следующих трех частей:

- имя объекта;

- состояние (переменные состояния);

- методы (операции).

Объект ООП - это совокупность переменных состояния и связанных с ними методов (операций). Эти методы определяют как объект взаимодействует с окружающим миром.

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

Синтаксис декларации объектов аналогичен базовому типу.

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

Понятие класса. Понятие класс (class) относится ко всем объектам, которые ведут себя одинаково. Например, все окружности имеют вполне определенную форму, они обладают такими атрибутами, как местоположение, цвет, диаметр. Объект – это конкретный экземпляр данного класса. Например, Земля имеет размер, цвет  и местоположение, отличные от аналогичных параметров для Луны или Солнца. Связь между классом и объектами в сущности такая же, как между типом и переменными этого типа.

Класс (class) - это группа данных и методов(функций) для работы с этими данными. Это шаблон. Объекты с одинаковыми свойствами, то есть с одинаковыми наборами переменных состояния и методов, образуют класс.

Каждый класс объектов может реагировать на строго определенные сообщения. Так происходит потому, что каждый класс обладает набором функций, которые связаны с объектами класса. Функции являются частью этого класса объектов – его членами. На рисунке показан объект, содержащий функции-члены. Программа посылает этому объекту сообщения (messages), которые вызывают функции-члены (member functions) данного объекта. Затем эти функции-члены обрабатывают объект.

Эти функции называются функциями-членами, поскольку принадлежат классу, то есть являются его членами. Функции-члены программируются так же, как обычные функции, однако объявляются в классе и могут использоваться только с объектами этого класса.

Примерная структура класса (не привязанная к какому-либо языку ООП):

Class  имя_класса  [ от кого унаследован]

{

private:

. . . . . . .

public:

. . . . . . .

protected:

. . . . . . .

}

Класс должен иметь уникальное имя. Если он наследован из другого, то надо указать имя родительского(их) класса(ов). Обычно у класса бывают три раздела: private, public, protected. Указание на начало раздела private часто опускается и, если не объявлено начало ни одного из других разделов описания класса, считается, что данные относятся к разделу private.

Методы в классе могут быть объявлены как дружественные (friend) или виртуальные (virtual). Иногда встречается объявление перегружаемых (overload) функций. Каждое из этих понятий более подробно мы рассмотрим отдельно.

Private (частный) раздел описания класса обычно находится вначале описания класса и содержит данные, доступ к которым закрыт из внешнего мира. Это и есть та самая "строго охраняемая" зона класса, доступ к которой можно получить только из методов самого класса. Она скрыта от внешнего мира глухой непробиваемой стеной и доступ к данным раздела private обеспечивается только с помощью, специально описанных в других разделах, методов. Скрытые в этом разделе данные также не доступны для всех производных классов.

Если бы все данные, описанные в базовом (родительском) классе, были доступны для производных классов, то можно было бы просто создать супер-класс, а затем из производных классов получать свободный доступ ко всем данным родительского класса. В то же время это перечеркнуло бы все наши старания по скрытию и защите данных. По этой причине, производные (наследуемые) классы автоматически не получают доступ к данным родительского класса (раздел private). Но бывают такие случаи, когда необходимо автоматически наследовать некоторые данные из родительского класса так, чтобы они вели себя так, как будто они описаны в производном классе . Именно для этих целей и существует раздел protected(защищенный) описания класса.

Protected (защищенный) - раздел описания класса содержит данные и методы, доступ к которым закрыт из внешней среды, но они напрямую доступны производным классам.

Таким образом, раздел protected используется для описания данных и методов, которые будут доступны только из производных классов. А в производных классах эти данные и методы воспринимаются, как если бы они были описаны в самом производном классе.

Название раздела public для англо-язычной публики говорит само за себя. Переводится как публичный, я бы сказал, открытый раздел. Методы описанные в разделе public доступны в пределах области видимости объекта и для производных классов. Таким образом, можно получить свободный доступ к методам, описанным в разделе public, из любого места программы (объект должен быть виден) и из любого производного класса. Методы, входящие в этот раздел, образуют интерфейс класса, с помощью которого и осуществляется взаимодействие экземпляра класса с внешним миром. Это единственный раздел, доступ к которому из внешней среды никак не ограничен.

Пример простейшего класса данных:

Class date

{

private:int,day,year

}

public: int, input (int,char,int);

int output (int, char*, int);

int sum1 (int,char*, int);

int sum2 (int, char*, int);

int min1 (int, Char*,int);

int min n (int,char*, int);

int koi (int, char*,int,int,char*,int,int)

При указании базового(родительского) класса в описании класса в С++ требуется указать ключевое слово public. Указание этого ключевого слова позволит получить свободный доступ ко всем методам класса, как если бы они были описаны в самом производном классе. В противном же случае, мы не сможем получить доступ к методам родительского класса.

Пример описания наследования классов на С++:

class A

{

. . . . .

}

class B : public A

{

. . . . .

}

Классы предоставляют программисту возможность моделировать объекты, которые имеют атрибуты (представленные как данные элементы) и варианты поведения или операции (представленные как функции элементы). Типы, содержащие данные-элементы и функции-элементы, обычно определяются в C++ с помощью ключевого слова class.

Пример 10.1. Использование абстрактного типа данных Time с помощью класса Time

Class Time

{

public:

   Time();

    void setTime(int, int, int);

    void printMilitary();

    void printStandard();

private:

    int hour;//0-23

    int minute;//0-59

    int second;//0-59

};

Листинг 10.1

//FIG6_3.cpp

//Класс Time

#include <iostream.h>

//Определение абстрактного типа данных (АТД) Time

class Time

{

public:

Time();

void setTime(int, int, int);//установка часов, минут и секунд

void printMilitary();//печать времени в военном формате

void printStandard();//печать времени в стандартном формате

private:

int hour; //0-23

int minute;//0-59

int second;//0-59

};

//Конструктор Time присваивает нулевые начальные значения каждому элементу данных. Обеспечивает согласованое начальное состояние всех объектов Time

Time::Time() {hour = minute = second = 0;}

//Задание нового значения Time в виде военного времени.

//Проверка правильности значений данных.

//Обнуление неверных значений.

void Time::setTime(int h, int m, int s)

{

hour = (h >= 0 && h < 24) ? h : 0;

minute = (m >= 0 && m < 60) ? m : 0;

second = (s >= 0 && s < 60) ? s : 0;

}

//Печать времени в военном формате

void Time::printMilitary()

{

cout << (hour < 10 ? "0":"")<<hour<<":"<<(minute < 10 ? "0" : "")<<minute << ":"<<(second < 10 ? "0" : "") << second;

}

//Печать времени в стандартном формате

void Time::printStandard()

{

cout << ((hour  == 0 || hour == 12) ? 12 : hour % 12)<<":"<<(minute < 10 ? "0" :"")<<minute <<":" <<(second < 10 ? "0" : "") << second << (hour  <  12  ? "AM"  :  " PM");

}

//Формирование проверки простого класса Time

main()

{

Time t; //определение экземпляра объекта t класса Time

cout << "Начальное значение военного времени равно ";

t.printMilitary();

cout << endl << "Начальное значение стандартного времени равно ";

t.printStandard();

t.setTime(13,27,6);

cout << endl << endl << "Военное время после setTime равно ";

t.printMilitary();

cout<<endl<<"Стандартное время после setTime равно ";

t.printStandard();

t.setTime(99,99,99);//попытка установить неправильные значения

cout << endl << endl <<"После попытки неправильной установки: " << endl << "Военное время: ";

t.printMilitary();

cout << endl << "Стандартное время: ";

t.printStandard();

cout << endl;

return 0;

}

Инкапсуляция.  Инкапсуляция - это механизм, который объединяет данные и методы, манипулирующие этими данными, и защищает и то и другое от внешнего вмешательства или неправильного использования. Когда методы и данные объединяются таким способом, создается объект.

Применяя инкапсуляцию, мы, защищаем данные, принадлежащие объекту, от возможных ошибок, которые могут возникнуть при прямом доступе к этим данным. Кроме того, применение этого принципа очень часто помогает локализовать возможные ошибки в коде программы. А это на много упрощает процесс поиска и исправления этих ошибок. Можно сказать, что инкапсуляция подразумевает под собой скрытие данных (data hiding), что позволяет защитить эти данные.

Переменные состояния объекта скрыты от внешнего мира. Изменение состояния объекта (его переменных) возможно ТОЛЬКО с помощью его методов (операций).

Этот принцип позволяет защитить переменные состояния объекта от неправильного их использования. Это существенно ограничивает возможность введения объекта в недопустимое состояние и/или несанкционированное разрушение этого объекта.

Хорошим примером применения принципа инкапсуляции являются команды доступа к файлам. Обычно доступ к данным на диске можно осуществить только через специальные функции. Вы не имеете прямой доступ к данным, размещенным на диске. Таким образом, данные, размещенные на диске, можно рассматривать скрытыми от прямого Вашего вмешательства. Доступ к ним можно получить с помощью специальных функций, которые по своей роли схожи с методами объектов. При этом, хотелось бы отметить два момента, которые важны при применении этого подхода. Во-первых, Вы можете получить все данные, которые Вам нужны за счет законченного интерфейса доступа к данным. И, во-вторых, Вы не можете получить доступ к тем данным, которые Вам не нужны. Это предотвращает случайную порчу данных, которая возможна при прямом обращении к файловой системе. Кроме того, это предотвращает получение неверных данных, т.к. специальные функции обычно используют последовательный доступ к данным.

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

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

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

Наследование (Inheritance). Наследование - это процесс, посредством которого один объект может наследовать свойства другого объекта и добавлять к ним черты, характерные только для него.

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

В описаниях языков ООП принято класс, из которого наследуют называть родительским классом (parent class) или основой класса (base class). Класс, который получаем в результате наследования называется порожденным классом (derived or child class). Родительский класс всегда считается более общим и развернутым. Порожденный же класс всегда более строгий и конкретный, что делает его более удобным в применении при конкретной реализации.

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

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

  1.  Создаётся иерархия классов, где классы стоящие ниже по иерархии могут иметь доступ к переменным и функциям выше стоящих классов.
  2.  Классы стоящие ниже по иерархии- производные классы, относительно классов, которые стоят выше них (4,5 - производные относительно 2, а  8,9 - производные относительно 7).
  3.  Классы , которые состоят выше по иерархиям являются базовыми для ниже стоящих классов (1 - базовый для 2 и 3).
  4.  Понятие базового и производного класса не предполагают относительность уровня иерархи, т.е. то множество классов, которое стоят на один уровень выше и являются базовыми.
  5.  Каждый производный класс имеет множество непосредственных родителей, т.е. то множество классов, которые стоят на один уровень выше и являются базовыми.
  6.  Соответственно: если родитель один- простое наследование, в другом же случае- множественное наследование.
  7.  Начало иерархии компьютера - это класс, один или более, которые называются протоклассом плюс корень дерева. Обычно бывает 2 или 3 протокласса на практике. Обычно протокол являются пустыми или состоят из пустых виртуальных функций. (Виртуальной называется функция ,сущность которой определяется во время выполнения программы.)
  8.  Классы стоящие ниже по иерархии имеют дополнительные свойства и функции относительно вышестоящих классов.

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

Когда один класс наследуется другим, используется следующая форма записи:

class имя_производного_класса: сп_доступа имя_базового_класса

{

//….

}

Здесь сп_доступа – это одно из трех ключевых слов: public, private или protected. Спецификатор доступа определяет, как элементы базового класса наследуются производным классом. Если спецификатором доступа наследуемого базового класса  является ключевое слово public, то все открытые члены базового класса остаются открытыми и в производном. Если спецификатором доступа наследуемого базового класса является ключевое слово private, то все открытыте члены базового класса в производном классе становятся закрытыми. В обоих случаях все закрытые члены базового класса в производном классе остаются закрытыми и недоступными. Важно понимать, что если спецификатором доступа является ключевое слово private, то хотя открытые члены базового класса становятся закрытыми в производном, они остаются доступными для функций – членов производного класса.

Технически сецификатор доступа необязателен.

Пример 10.2. Реализация принципа наследования классов

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

Листинг 10.2

#include <iostream.h>

class Subtraction

{

public:

int funct_Sub(int a, int b);

};

class Sum:public Subtraction

{

public:

int funct_Sum(int a, int b);

};

int Subtraction::funct_Sub(int a, int b)

{

return a - b;

}

int Sum::funct_Sum(int a, int b)

{

return a + b;

}

main()

{

Sum s;

int a = 5;

int b = 3;

cout << "Raznost " << s.funct_Sub(a,b) << endl;

cout << "Summa " << s.funct_Sum(a,b) << endl;

 return 0;

}

Полиморфизм. Полиморфизм - это свойство, которое позволяет одно и тоже имя использовать для решения нескольких технически разных задач.

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

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

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

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

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

Операторы объектно-ориентированного программирования, связанные с применением классов:

1. Оператор доступа (.)

Синтаксис:

переменная типа класс.член класса ;

Доступ по этому оператору извне возможен только к октрытому классу public. Под “извне” понимается внешняя функция для класса

2. Оператор видимости (::)

Назначение оператора – определить к какому классу относиться конкретная функция.

Синтаксис:

Тип имя_класса :: имя_функции (список параметров с указанием типа)

{

тело функции

}

Оператор видимости трансформирует имя_функции в имя_класса + имя_функции.

3. Операция стрелка доступа к членам класса

Используется, если объект объявлен как указатель на класс.

y *obj;

obj input ( ); эквивалентно (*obj). input ( );

Синтаксис оператора “стрелка”:

адрес_объекта член_класса;

при объявлении объекта: имя_класса*имя_объекта.

4. Указатель this

Используется только в функциях членах класса. Указатель возвращает объект (адрес объекта), для которого функция применяется.

Конструкторы. Допустим, имеется объект класса Clock. При объявлении этого объекта, он автоматически инициализуется. Это означает, что при создании нового объекта класса Clock переменной timestarted присваивается текущее системное время. Кто (или вернее что) это делает?

Для этого нужно определить специальную функцию, которая будет специально вызываться при создании каждого объекта. В языке С++ это можно сделать при помощи специальной функции, которая называется конструктором (constructor).

Конструктор похож на любую другую функцию-член, за исключением следующего:

  1.  Имя конструктора совпадает с именем класса. Например, конструктором класса Clock является функция Clock().
  2.  При создании нового объекта конструктор вызывается автоматически. Например, если создать два объекта mine и yours класса Clock, то конструктор Clock() будет вызван дважды- один раз при создании объекта mine и другой при создании объекта yours.
  3.  Конструктор нельзя вызвать из программы напрямую. Например, нельзя написать инструкцию mine.Clock(); Конструктор вызывается только однажды – при создании объекта.
  4.  У конструктора нет возвращаемого типа. Возможно существование нескольких конструкторов с разными списками аргументов.

Простейшие правила проектирования класса:

  1.  Переменные класса находятся в разделе privat.
  2.  Для каждой переменной класса в классе должна быть функция установки.
  3.  Функции установки обычно являются открытыми.
  4.  Для каждой закрытой переменной класса в классе должна быть функция доступа.
  5.  Функция доступа (обычно) расположена в открытой части класса.

Встроенными функциями (in line) называются функции класса, описанные внутри класса, то есть тело функции находится внутри класса. Встроенными могут быть функции, которые не содержат сложных операций if, вложенных в цепи.

Простейший класс:

Class my:

{

public

int x,y;

 publiс:

inline     int funk 1(void)

{

retun(x+y);

}

    int funk 2(void)

{

retun(x*y);

}

void set x(int var)

{

x = var;

}

void set y(int var)

{

y = var;

}

int ret x(void)

{

return x;

}

int ret y(void)

{

return y;

}

}

Функция узнается компилятором по двойным фигурным скобкам. Это объясняет их необходимость.

Описание конструктора составляет альтернативу использованию нескольких функций (перегруженных), который по заданному double создает complex.

Например:

class complex

{

// ...

complex(double r)

{

re = r;

im = 0;

}

};

Конструктор, требующий только один параметр, необязательно вызывать явно:

complex z1 = complex(23);

complex z2 = 23;

И z1, и z2 будут инициализированы вызовом complex(23).

Конструктор - это предписание, как создавать значение данного типа. Когда требуется значение типа, и когда такое значение может быть создано конструктором, тогда, если такое значение дается для присваивания, вызывается конструктор. Например, класс complex можно было бы описать так:  

class complex 

{

double re, im;

 public:

complex(double r, double i = 0)

{

re = r;

im = i;

}

friend complex operator+(complex, complex);

friend complex operator*(complex, complex);

};

и действия, в которые будут входить переменные complex и целые константы, стали бы допустимы. Целая константа будет интерпретироваться как complex с нулевой мнимой частью. Например, a = b*2 означает:

a = operator*( b, complex( double(2), double(0) ) )

Определенное пользователем преобразование типа применяется неявно только тогда, когда оно является единственным.

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

Операции Преобразования. Использование конструктора для задания преобразования типа является удобным, но имеет следствия, которые могут оказаться нежелательными:

1. Не может быть неявного преобразования из определенного пользователем типа в основной тип (поскольку основные типы не являются классами);

2. Невозможно задать преобразование из нового типа в старый, не изменяя описание старого;

3. Невозможно иметь конструктор с одним параметром, не имея при этом преобразования.

Последнее не является серьезной проблемой, а с первыми двумя можно справиться, определив для исходного типа операцию преобразования. Функция член X::operator T(), где T - имя типа, определяет преобразование из X в T.

Например, можно определить тип tiny (крошечный), который может иметь значение только в диапазоне 0...63, но все равно может свободно сочетаться в целыми в арифметических операциях:

class tiny

{

char v;

int assign(int i)

{

return v = (i&~63) ? (error("ошибка диапазона"),0) : i;

}

  public:

tiny(int i)            

{

assign(i);

}

      tiny(tiny& i)          

{

v = t.v;

}

int operator=(tiny& i)

{

return v = t.v;

}

int operator=(int i)   

{

  return assign(i);

}

operator int()         

{

return v;

}

}

Диапазон значения проверяется всегда, когда tiny инициализируется int, и всегда, когда ему присваивается int. Одно tiny может присваиваться другому без проверки диапазона. Чтобы разрешить выполнять над переменными tiny обычные целые операции, определяется tiny::operator int(), неявное преобразование из int в tiny. Всегда, когда в том месте, где требуется int, появляется tiny, используется соответствующее ему int. Например:

  

void main()

{

tiny c1 = 2;

tiny c2 = 62;

tiny c3 = c2 - c1;  // c3 = 60

tiny c4 = c3;    // нет проверки диапазона (необязательна)

int i = c1 + c2;    // i = 64

c1 = c2 + 2 * c1;   // ошибка диапазона: c1 = 0 (а не 66)

c2 = c1 -i;         // ошибка диапазона: c2 = 0

c3 = c2;        // нет проверки диапазона (необязательна)

}

Тип вектор из tiny может оказаться более полезным, поскольку он экономит пространство. Чтобы сделать этот тип более удобным в обращении, можно использовать операцию индексирования.

Другое применение определяемых операций преобразования - это типы, которые предоставляют нестандартные представления чисел  (арифметика по основанию 100, арифметика с фиксированной точкой, двоично-десятичное представление и т.п.). При этом обычно переопределяются такие операции, как + и *.

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

Типы istream и ostream опираются на функцию преобразования, чтобы сделать возможными такие операторы, как while (cin>>x) cout<>x выше возвращает istream&. Это значение неявно преобразуется к значению, которое указывает состояние cin, а уже это значение может проверяться оператором while. Однако определять преобразование из оного типа в другой так, что при этом теряется информация, обычно не стоит.

Неоднозначности. Присваивание объекту (или инициализация объекта) класса X является допустимым, если или присваиваемое значение является X, или существует единственное преобразование присваиваемого значения в тип X.

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

  

class x

{

/* ... */ x(int); x(char*);

};

class y

{

/* ... */ y(int);

};

class z

{

/* ... */ z(x);

};

overload f;

x f(x);

y f(y);

z g(z);

f(1);         // недопустимо: неоднозначность f(x(1)) или f(y(1))

f(x(1));

f(y(1));

g("asdf");     // недопустимо: g(z(x("asdf"))) не пробуется

g(z("asdf"));

Определенные пользователем преобразования рассматриваются только в том случае, если без них вызов разрешить нельзя. Например:

  

class x

{

/* ... */ x(int);

}

overload h(double), h(x);

h(1);

Вызов мог бы быть проинтерпретирован или как h(double(1)), или как h(x(1)), и был бы недопустим по правилу единственности. Но первая интерпретация использует только стандартное преобразование. Правила преобразования не являются ни самыми простыми для реализации и документации, ни наиболее общими из тех, которые можно было бы разработать. Возьмем требование единственности преобразования. Более общий подход разрешил бы компилятору применять любое преобразование, которое он сможет найти; таким образом, не нужно было бы рассматривать все возможные преобразования перед тем, как объявить выражение допустимым. К сожалению, это означало бы, что смысл программы зависит от того, какое преобразование было найдено. В результате смысл программы неким образом зависел бы от порядка описания преобразования.

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

Самый общий подход учитывал бы всю имеющуюся информацию о типах и рассматривал бы все возможные преобразования. Например, если использовать предыдущее описание, то можно было бы обработать aa=f(1), так как тип aa определяет единственность толкования. Если aa является x, то единственное, дающее в результате x, который требуется присваиванием, - это f(x(1)), а если aa - это y, то вместо этого будет использоваться f(y(1)). Самый общий подход справился бы и с g("asdf"), поскольку единственной интерпретацией этого может быть g(z(x("asdf"))). Сложность этого подхода в том, что он требует расширенного анализа всего выражения для того, чтобы определить интерпретацию каждой операции и вызова функции. Это приведет к замедлению компиляции, а также к вызывающим удивление интерпретациям и сообщениям об ошибках, если компилятор рассмотрит преобразования, определенные в библиотеках и т.п.

Константы. Константы классового типа определить невозможно в том смысле, в каком 1.2 и 12e3 являются константой типа double. Вместо них, однако, часто можно использовать константы основных типов, если их реализация обеспечивается с помощью функций членов. Общий аппарат для этого дают конструкторы, получающие один параметр. Когда конструкторы просты и подставляются inline, имеет смысл рассмотреть в качестве константы вызов конструктора. Если, например, в есть описание класса comlpex, то выражение zz1*3+zz2*comlpex(1,2) даст два вызова функций, а не пять. К двум вызовам

функций приведут две операции *, а операция + и конструктор, к которому обращаются для создания comlpex(3) и comlpex(1,2), будут расширены inline.

Большие Объекты. При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр передается копия каждого операнда. Расходы на копирование каждого double заметны, но с ними вполне можно примириться. К сожалению, не все классы имеют небольшое и удобное представление. Чтобы избежать ненужного копирования, можно описать функции таким образом, чтобы они получали ссылочные параметры. Например:

class matrix

{

double m[4][4];

  public:

matrix();

friend matrix operator+(matrix&, matrix&);

friend matrix operator*(matrix&, matrix&);

};

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

matrix operator+(matrix&, matrix&);

{

matrix sum;

for (int i=0; i<4; i++)

for (int j=0; j<4; j++)

sum.m[i][j] = arg1.m[i][j] + arg2.m[i][j];

return sum;

}

Эта operator+() обращается к операндам + через ссылки, но возвращает значение объекта. Возврат ссылки может оказаться более эффективным:

class matrix

{

// ...

friend matrix& operator+(matrix&, matrix&);

friend matrix& operator*(matrix&, matrix&);

};

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

Присваивание и Инициализация. Рассмотрим очень простой класс строк string:

struct string

{

char* p;

int size;    // размер вектора, на который указывает p

string(int sz)

{

p = new char[size=sz];

}

~string()

{

delete p;

}

};

Строка - это структура данных, состоящая из вектора символов и длины этого вектора. Вектор создается конструктором и уничтожается деструктором.

Однако это может привести к неприятностям. Например:

void f()

{

string s1(10);

string s2(20);

s1 = s2;

}

будет размещать два вектора символов, а присваивание s1 = s2 будет портить указатель на один из них и дублировать другой. На выходе из f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот же вектор с непредсказуемо разрушительными последствиями. Решение этой проблемы состоит в том, чтобы соответствующим образом определить присваивание объектов типа string:

struct string 

{

char* p;

int size;    // размер вектора, на который указывает p

string(int sz)

{

p = new char[size = sz];

}

~string()

{

delete p;

}

void operator = (string&)

};

void string::operator = (string& a)

{

if (this == &a) return;   // остерегаться s =s;

delete p;

p = new char[size=a.size];

strcpy(p,a.p);

}

Это определение string гарантирует, и что предыдущий пример будет работать как предполагалось. Однако небольшое изменение f() приведет к появлению той же проблемы в новом облике:

void f()

{

string s1(10);

s2 = s1;

}

Теперь создается только одна строка, а уничтожается две. К неинициализированному объекту определенная пользователем операция присваивания не применяется. Беглый взгляд на string::operator = () объясняет, почему было неразумно так делать: указатель p будет содержать неопределенное и совершенно случайное значение. Часто операция присваивания полагается на то, что ее аргументы инициализированы. Для такой инициализации, как здесь, это не так по определению. Следовательно, нужно определить похожую, но другую, функцию, чтобы обрабатывать инициализацию:

struct string

{

char* p;

int size;    // размер вектора, на который указывает p

string(int sz)

{

p = new char[size = sz];

}

~string()

{

delete p;

}

void operator = (string&)

string(string&);

};

void string::string(string& a)

{

P = new char[size = a.size];

strcpy(p,a.p);

}

Для типа X инициализацию тем же типом X обрабатывает конструктор X(X&).

Присваивание и инициализация – разные действия. Это особенно существенно при описании деструктора. Если класс X имеет конструктор, выполняющий нетривиальную работу вроде освобождения памяти, то скорее всего потребуется полный комплект функций, чтобы полностью избежать побитового копирования объектов:

class X

{

// ...

X(something);  // конструктор: создает объект

X(&X);         // конструктор: копирует в инициализации

operator=(X&); // присваивание: чистит и копирует

~X();          // деструктор: чистит

}

Есть еще два случая, когда объект копируется: как параметр функции и как возвращаемое значение. Когда передается параметр, инициализируется неинициализированная до этого переменная - формальный параметр. Семантика идентична семантике инициализации. То же самое происходит при возврате из функции, хотя это менее очевидно. В обоих случаях будет применен X(X&), если он определен:

string g(string arg)

{

return arg;

}

main()

{

string s = "asdf";

s = g(s);

}

Ясно, что после вызова g() значение s обязано быть "asdf". Копирование значения s в параметр arg сложности не представляет: для этого надо взывать string(string&). Для взятия копии этого значения из g() требуется еще один вызов string(string&); на этот раз инициализируемой является временная переменная, которая затем присваивается s. Такие переменные, естественно, уничтожаются как положено с помощью string::~string() при первой возможности.

Индексирование. Чтобы задать смысл индексов для объектов класса используется функция operator[]. Второй параметр (индекс) функции operator[] может быть любого типа. Это позволяет определять ассоциативные массивы и т.п.

Пример 10.2. Реализация принципа индексирования классов

Написать программу для подсчета числа вхождений слов в файл с применением ассоциативного массива и использованием функции.

Здесь определяется надлежащий тип ассоциативного массива:

struct pair 

{

char* name;

int val;

};

class assoc

{

pair* vec;

int max;

int free;

public:

assoc(int);

int& operator[](char*);

void print_all();

}

В assoc хранится вектор пар pair длины max. Индекс первого неиспользованного элемента вектора находится в free. Конструктор выглядит так:

assoc::assoc(int s)

{

max = (s<16) ? s : 16;

free = 0;

vec = new pair[max];

}

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

Листинг 10.3

#include

int assoc::operator[](char* p)

/*

работа с множеством пар "pair":

поиск p,

возврат ссылки на целую часть его "pair"

делает новую "pair", если p не встречалось

*/

{

register pair* pp;

for (pp = &vec[free-1]; vec <= pp; pp--)

if (strcmp(p,pp->name)==0) return pp->val;

if (free == max) {    // переполнение: вектор увеличивается

pair* nvec = new pair[max*2];

for (int i = 0; iname = new char [strlen(p)+1];i++);

strcpy(pp->name,p);

pp->val = 0;     // начальное значение: 0

return pp->val;

}

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

vouid assoc::print_all()

{

for (int i = 0; i>buf) vec[buf]++;

vec.print_all();

}

Вызов Функции. Вызов функции, то есть запись выражение(список_выражений), можно проинтерпретировать как бинарную операцию, и операцию вызова можно перегружать так же, как и другие операции. Список параметров функции operator() вычисляется и проверяется в соответствие с обычными правилами передачи параметров. Перегружающая функция может оказаться полезной главным образом для определения типов с единственной операцией и для типов, у которых одна операция настолько преобладает, что другие в большинстве ситуаций можно не принимать во внимание.

Для типа ассоциативного массива assoc мы не определили итератор. Это можно сделать, определив класс assoc_iterator, работа которого состоит в том, чтобы в определенном порядке поставлять элементы из assoc. Итератору нужен доступ к данным, которые хранятся в assoc, поэтому он сделан другом:

class assoc

{

friend class assoc_iterator;

pair* vec;

int max;

int free;

public:

assoc(int);

int& operator[](char*);

};

Итератор определяется как

class assoc_iterator

{

assoc* cs;  // текущий массив assoc

int i;      // текущий индекс

public:

assoc_iterator(assoc& s)

{

cs = &s; i = 0;

}

pair* operator()()

{

return (ifree)? &cs->vec[i++] : 0;

}

};

Надо инициализировать assoc_iterator для массива assoc, после чего он будет возвращать указатель на новую pair из этого массива всякий раз, когда его будут активизировать операцией (). По достижении конца массива он возвращает 0:

main()    // считает вхождения каждого слова во вводе

{

const MAX = 256;  // больше самого большого слова

char buf[MAX];

assoc vec(512);

while (cin>>buf) vec[buf]++;

assoc_iterator next(vec);

pair* p;

while ( p = next() )

cout << p->name << ": " << p->val << "\n";

}

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

Конечно, такое применение объектов для представления итераторов никак особенно с перегрузкой операций не связано. Многие любят использовать итераторы с такими операциями, как first(), next() и last() (первый, следующий и последний).

Пример 10.3. Класс Строка

Написать программу с использованием класса string, в котором  производится учет ссылок на строку с целью минимизировать копирование. В качестве констант применяются стандартные символьные строки C++.

Листинг 10.4

#include

#include

class string

{

struct srep

{

char* s;           // указатель на данные

int   n;           // счетчик ссылок

  };

srep *p;

public:

string(char *);        // string x = "abc"

string();              // string x;

string(string &);      // string x = string ...

string& operator=(char *);

string& operator=(string &);

~string();

char& operator[](int i);

friend ostream& operator<<(ostream&, string&);

friend istream& operator>>(istream&, string&);

friend int operator==(string& x, char* s)

{

return strcmp(x.p->s, s) == 0;

}

friend int operator==(string& x, string& y)

{

return strcmp(x.p->s, y.p->s) == 0;

}

friend int operator!=(string& x, char* s)

{

return strcmp(x.p->s, s) != 0;

}

friend int operator!=(string& x, string& y)

{

return strcmp(x.p->s, y.p->s) != 0;

}

};

Конструкторы и деструкторы просты (как обычно):

 

string::string()

{

p = new srep;

p->s = 0;

p->n = 1;

}

string::string(char* s)

{

p = new srep;

p->s = new char[ strlen(s)+1 ];

strcpy(p->s, s);

p->n = 1;

}

string::string(string& x)

{

x.p->n++;

p = x.p;

}

string::~string()

{

if (--p->n == 0)

{

delete p->s;

delete p;

}

}

Как обычно, операции присваивания очень похожи на конструкторы. Они должны обрабатывать очистку своего первого (левого) операнда:

string& string::operator=(char* s)

{

if (p->n > 1)

{    // разъединить себя

p-n--;

p = new srep;

}

else if (p->n == 1)

delete p->s;

p->s = new char[ strlen(s)+1 ];

strcpy(p->s, s);

p->n = 1;

return *this;

}

Благоразумно обеспечить, чтобы присваивание объекта самому себе работало правильно:

string& string::operator=(string& x)

{

x.p->n++;

if (--p->n == 0)

{

delete p->s;

delete p;

}

p = x.p;

return *this;

}

Операция вывода задумана так, чтобы продемонстрировать применение учета ссылок. Она повторяет каждую вводимую строку (с помощью операции <<, которая определяется позднее):

ostream& operator<<(ostream& s, string& x)

{

return s << x.p->s << " [" << x.p->n << "]\n";

}

Операция ввода использует стандартную функцию ввода символьной строки:

istream& operator>>(istream& s, string& x)

{

char buf[256];

s >> buf;

x = buf;

cout << "echo: " << x << "\n";

return s;

}

Для доступа к отдельным символам предоставлена операция индексирования. Осуществляется проверка индекса:

void error(char* p)

{

cerr << p << "\n";

exit(1);

}

char& string::operator[](int i)

{

if (i<0 || strlen(p->s)s[i];

}

Головная программа просто немного опробует действия над строками. Она читает слова со ввода в строки, а потом эти строки печатает. Она продолжает это делать до тех пор, пока не распознает строку done, которая завершает сохранение слов в строках, или не встретит конец файла. После этого она печатает строки в обратном порядке и завершается.

main()

{

string x[100];

int n;

cout << "отсюда начнем\n";

for (n = 0; cin>>x[n]; n++)

{

string y;

if (n==100) error("слишком много строк");

cout << (y = x[n]);

if (y=="done") break;

}

cout << "отсюда мы пройдем обратно\n";

for (int i=n-1; 0<=i; i--) cout << x[i];

}

Друзья и Члены. Теперь, наконец, можно обсудить, в каких случаях для доступа к закрытой части определяемого пользователем типа использовать члены, а в каких - друзей. Некоторые операции должны быть членами: конструкторы, деструкторы и виртуальные функции, но обычно это зависит от выбора.

Рассмотрим простой класс X:

class X

{

// ...

X(int);

int m();

friend int f(X&);

};

Внешне не видно никаких причин делать f(X&) другом дополнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для "настоящего объекта", в то время как друг f() может вызываться для объекта, созданного с помощью неявного преобразования типа. Например:

void g()

{

1.m();      // ошибка

f(1);       // f(x(1));

}

Поэтому операция, изменяющее состояние объекта, должно быть членом, а не другом. Для определяемых пользователем типов операции, требующие в случае фундаментальных типов операнд lvalue (=, *=, ++ и т.д.), наиболее естественно определяются как члены. И наоборот, если нужно иметь неявное преобразование для всех операндов операции, то реализующая ее функция должна быть другом, а не членом. Это часто имеет место для функций, которые реализуют операции, не требующие при применении к фундаментальным типам lvalue в качестве операндов (+, -, || и т.д.).

Если никакие преобразования типа не определены, то оказывается, что нет никаких существенных оснований в пользу члена, если есть друг, который получает ссылочный параметр, и наоборот. В некоторых случаях программист может предпочитать один синтаксис вызова другому. Например, оказывается, что большинство предпочитает для обращения матрицы m запись m.inv().

Конечно, если inv() действительно обращает матрицу m, а не просто возвращает новую матрицу, обратную m, ей следует быть другом.

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

Невозможно предсказать, потребуют ли будущие изменения изменить статус объекта. Синтаксис вызова функции члена ясно указывает пользователю, что объект можно изменить; ссылочный параметр является далеко не столь очевидным. Кроме того, выражения в члене могут быть заметно короче выражений в друге. В функции друге надо использовать явный параметр, тогда как в члене можно использовать неявный this. Если только не применяется перегрузка, имена членов обычно короче имен друзей.

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

Пример 10.4. Реализация принципа композиции классов

Написать программу для вывода на экран определенных значений.

Листинг 10.5

#include <iostream.h>

class One

{

public:

One(int = 1);//конструктор по умолчанию

void print();

private:

int a;

};

class Two

{

public:

Two(int = 1);//конструктор по умолчанию

void print();

private:

int a;

};

class OnePlusTwo_Three

{

public:

OnePlusTwo_Three(int=1, int=1);//конструктор по умолчанию

void print();

private:

One o;

Two t;

};

One::One(int a1)

{

a = a1;

}

void One::print()

{

cout << a << endl;

}

void Two::print()

{

cout << a << endl;

}

Two::Two(int a2)

{

a = a2;

}

OnePlusTwo_Three::OnePlusTwo_Three(int a1, int a3):o(a1),t(a3)

{

}

void OnePlusTwo_Three::print()

{

o.print();

t.print();

}

main()

{

OnePlusTwo_Three opt(6,8);

opt.print();

 return 0;

}

В этой программе определено три класса: One, Two, OnePlusTwo_Three.

Композиция классов в этом примере реализована в том, что мы включили под директивой private в классе OnePlusTwo_Three, два объекта классов: Two t, One o. А также посмотрев на определение конструктора класса OnePlusTwo_Three мы видим, что он содержит параметры, помогающие определить конструкторы классов One и Two.

Использование дружественных функций и указателя this. Дружественные функции определяются вне области действия этого класса, но имеют право доступа к закрытым элементам private данного класса. Функция или класс в целом могут быть объявлены другом (friend) другого класса.

Дружественные функции используются для повышения производительности.

Чтобы объявить функцию как друга (friend) класса, перед ее прототипом в описании класса ставится ключевое слово friend. Чтобы объявить класс ClassTwo как друга класса ClassOne, запишите объявление в форме friend ClassTwo  в определении класса ClassOne.

Дружественность требует разрешения, то есть чтобы класс B стал другом класса A, класс A должен объявить, что класс B - его друг. Таким образом дружественность не обладает ни свойством симметричности, ни свойством транзитивности, то есть если класс A друг класса B , а класс B - друг класса C, то от сюда не следует, что класс B друг класса A, что класс C друг класса B, или что класс A - друг класса C.

Ниже приведенная программа демонстрирует объявление и использование дружественной функции setX для установки закрытого элемента данных x класса count. Заметим, что объявление friend появляется первым (по соглашению) в объявлении класса, даже раньше объявления закрытых функций элментов.

Пример 10.5. Реализация дружественности классов

Написать программу, выводящую на экран целое значение.

Листинг 10.6

#include <iostream.h>

class One

{

friend class Two;

public:

One(int = 1);

private:

int a;

};

class Two

{

public:

int ret_value(One o1, int v);

};

One::One(int a1)

{

a = a1;

}

int Two::ret_value(One o1, int v)

{

o1.a = v;

return v;

}

main()

{

Two t;

One o1;

int v, r;

cout << "Enter the number what you want to see later! " << endl;

cin >> v;

r = t.ret_value(o1,v);

cout << endl;

cout << r << endl;

return 0;

}

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

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

Указатель this неявно используется для ссылки как на данные элементы так и на функции - элементы объекта. Тип указателя this зависит от типа объекта и от того, объявлена ли функция элемент, в которой используется this, как const. Например, в не константной функции-элементе класса Employee указатель this имеет тип Employee *const(константный указатель на объект Employee). В  константной функции-элементе класса Employee указатель this имеет тип const Employee *const(константный указатель на объект Employee, который тоже константный).

Пример 10.6. Использование указателя this

Написать программу, которая демонстрирует явное использование указателя this, чтобы дать возможность функции элементу класса Test печатать закрытую переменную x объекта Test.

Листинг 10.7

#include <iostream.h>

class Test

{

public:

Test (int = 0);

void print() const;

private:

int x;

};  

Test::Test(int a) {x = a;}//конструктор

void Test::print() const

{

cout <<”x = “<< x << endl <<”this->x = “ << this -> x<<endl <<”(*this).x=”<<(*this).x<<endl;

}

main()

{

Test a(12);

a.print();

return 0;

}

Перегрузка операций. Любая операция, определенная в C++, может быть перегружена для созданного класса. Это делается с помощью функций специального вида, называемых функциями-операциями (операторными функциями). Общий вид такой функции:

возвращаемый_тип operator # (список параметров)

{

тело функции

}

где вместо знака # ставится знак перегруаемой операции.

Функция-операция может быть реализована либо как функция класса, либо как внешняя (обычно дружественная) функция. В первом случае количество параметров у функции-операции на единицу меньше, так как первым операндом при этом считается сам объект, вызвавший данную операцию.

Например, два варианта перегрузки операции сложения для класса Point: первый вариант - в форме метода класса:

class Point

{

double x. у: public:

//...

Point operator +(Point&);

};

Point Point::operator +(Point& p)

{

return Point(x + p.x, у + р.у);

}

Второй вариант - в форме внешней глобальной функции, причем функция, как правило, объявляется дружественной классу, чтобы иметь доступ к его закрытым элементам:

class Point

{

double x, у;

public:

//. . .

friend Point operator +(Point&. Point&);

};

Point operator +(Point& p1.  Points p2)

{

return Point(p1.x + p2.x. p1.у + p2.y);

}

Независимо от формы реализации операции «+» можно теперь написать:

Point pl(0, 2), р2(-1, 5);

Point рЗ = p1 + р2;

Встретив выражение pi + р2, компилятор в случае первой формы перегрузки вызовет метод p1.operator + (p2), а в случае второй формы перегрузки - глобальную функцию operator  + (p1, р2).

Результатом выполнения данных операторов будет точка рЗ с координатами х = -1, у = 7.

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

Перегрузка операций инкремента (декремента). Операция инкремента (декремента) имеет две формы: префиксную и постфиксную. Для первой формы сначала изменяется состояние объекта в соответствии с данной операцией, а затем он (объект) используется в том или ином выражении. Для второй формы объект используется в том состоянии, которое у него было до начала операции, а потом уже его состояние изменяется.

Чтобы компилятор смог различить эти две формы операции инкремента (декремента), для них используются разные сигнатуры, например:

Point& operator ++(); // префиксный инкремент

Point operator ++(int);  // постфиксный инкремент

Реализация данных операций на примере класса Point:

Point& Point::operator ++()

{

x++;

y++;

return *this;

}

Point Point::operator ++(int)

{

Point old = *this;

X++;

y++;

return old;

}

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

Использование префиксного инкремента (декремента) для параметра цикла for дает более эффективный программный код.

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

Операция присваивания может быть определена только в форме метода класса и она, в отличие от всех остальных операций, не наследуется.

Например, для класса Man перегрузку операции присваивания можно определить следующим образом:

// Man.h (интерфейс класса) class Man 

{

public:

// . . .

Man& operator =(const Man&): //операция присваивания private:

char* pName;

II  .   .   .

};

// Маn.срр (реализация класса) // ...

Man& Man::operator =(const Man& man)

{

if (this == &man) return *this; // проверка на самоприсваивание

delete [] pName;  //уничтожить предыдущее значение

pName = new char[strlen(man.pName) + 1];

strcpy(pName. man.pName);

birth_year = man.birth_year;   

pay = man.pay;

return *this;

}

Моменты реализации операции присваивания:

 - убедитесь, что не выполняется присваивание вида х = х. Если левая и правая части ссылаются на один и тот же объект, то делать ничего не надо. Если не перехватить этот особый случай, то следующий шаг уничтожит значение, на
которое указывает
pName, еще до того, как оно будет скопировано;

 - удалите предыдущие значения полей в динамически выделенной памяти;

 - выделите память под новые значения полей;

 - скопируйте в нее новые значения всех полей;

 - возвратите значение объекта, на которое указывает this (то есть *this).

Статические элементы класса. До сих пор одноименные поля разных объектов одного и того же класса были уникальными. Но что делать, если необходимо создать переменную, значение которой будет общим для всех объектов конкретного класса? Если воспользоваться глобальной переменной, то это нарушит принцип инкапсуляции данных. Модификатор static как раз и позволяет объявить поле в классе, которое будет общим для всех экземпляров класса. Кроме объявления статического поля в классе, необходимо также дать его определение в глобальной области видимости программы, например:

class Coo 

{

static int count: // объявление в классе // остальной код

};      

int Coo::count = 1;  // определение и инициализация

// int Coo::count;  // по умолчанию инициализируется нулем

Аналогично статическим полям могут быть объявлены и статические методы класса (с модификатором static). Они могут обращаться непосредственно только к статическим полям и вызывать только другие статические методы класса, потому что им не передается скрытый указатель this. Статические методы не могут быть константными (const) и виртуальными (virtual). Обращение к статическим методам производится так же, как к статическим полям - либо через имя класса, либо, если хотя бы один объект класса уже создан, через имя объекта.

Пример 10.7. Класс треугольников

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

Для реализации этой задачи составить описание класса треугольников на плоскости. Предусмотреть возможность объявления в клиентской программе (main) экземпляра треугольника с заданными координатами вершин. Предусмотреть наличие в классе методов, обеспечивающих: 1) перемещение треугольников на плоскости; 2) определение отношения > для пары заданных треугольников (мера сравнения - площадь треугольников); 3) определение отношения включения типа: «Треугольник 1 входит в (не входит в) Треугольник 2».

Программа должна содержать меню, позволяющее осуществить проверку всех методов класса.

Применим гибридный подход: разработку главного клиента main( ) проведем по технологии функциональной декомпозиции, а функции-серверы, вызываемые из main(), будут использовать объекты.

Начнем с выявления основных понятий/классов. Первый очевидный класс Triangle необходим для представления треугольников (через три точки, задающие его вершины). Точку на плоскости представим с помощью пары вещественных чисел, задающих координаты точки по осям х и у.

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

Итак, объектно-ориентированная декомпозиция дала нам два класса: Triangle и Point.

Если класс В является «частным случаем» класса А, то говорят, что В is а А (напри мер, класс треугольников есть частный вид класса многоугольников: Triangle is a Polygon).

Если класс А содержит в себе объект класса В, то говорят, что A has а В (например, класс треугольников может содержать в себе объекты класса точек: Triangle has a Point).  

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

Займемся теперь основным клиентом - main(). Здесь мы применяем функциональную декомпозицию, или технологию нисходящего проектирования. В соответствии с данной технологией основной алгоритм представляется как последовательность нескольких подзадач. Каждой подзадаче соответствует вызываемая серверная функция. На начальном этапе проектирования тела этих функций могут быть заполнены «заглушками» - отладочной печатью. Если при этом в какой-то серверной функции окажется слабое сцепление, то она в свою очередь разбивается на несколько подзадач.

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

На первом этапе мы напишем код для начального представления классов Point и Triangle, достаточный для того, чтобы создать несколько объектов типа Triangle и реализовать первый пункт меню - вывод всех объектов на экран.

Этап 1

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

// Проект Task1_2

/////////Point.h #ifndef POINT_H #define POINT_H

class Point

{    

public:

// Конструктор

Point(double _x = 0. double _y = 0) : x(_x), y(_y) {}

// Другие методы

void Show() const; public:

double x, y:

};

#endif /* POINT_H */

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

// Point.cpp

#include <iostream>

#include "Point.h"

using namespace std;      

void Point::Show() const

{

cout « " (" «x « ","« у <<")";

}

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

Triangle.h #ifndef TRIANGLE_H #define TRIANGLE_H

#include "Point.h"

class Triangle

{

public:

Triangle(Point, Point, Point, const char*); // конструктор

Triangle(const char*);    // конструктор пустого (нулевого) треугольника

~Triangle( ); // деструктор

Point Get_vl() const 

{

return vl; 

}  // Получить значение vl

Point Get_v2() const

{

return v2;

}  // Получить значение v2

Point Get_v3() const

{

return v3; 

}  // Получить значение v3

char* GetName( ) const

{

return name; 

} // Получить имя объекта

void Show() const; // Показать объект

void ShowSideAndArea() const;      // Показать стороны и площадь объекта

public:

static int count;  // кол-во созданных объектов

private:

char* objID;       // идентификатор объекта
char* name;        // наименование треугольника
Point vl, v2, v3;    // вершины
double a;      // сторона, соединяющая vl и v2
double b; // сторона, соединяющая v2 и v3

double с;      // сторона, соединяющая vl и v3

};

#endif /* TRIANGLE_H */

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

//Triangle.cpp

// Реализация класса Triangle

#include <math.h>

#include <iostream>

#inc1ude <iomanip>

#include <cstring>

//#include "CyrIOS.h". // for Visual C++ 6.0

#include "Triangle.h"

using namespace std;

// Конструктор

Triangle: :Triangle(Point _v1.  Point _v2,  Point _v3, const char* ident)

: vl'(_vl), v2(_v2). v3(_v3)

{

char buf[16];

objID = new char[strlen(ident) + 1];

strcpy(objID.  ident);

count++;

sprintf(buf. "Треугольник %d", count);

name = new char[strlen(buf) + 1];

strcpy(name, buf);

a = sqrt((vl.x - v2.x) * (vl.x - v2.x) + (vl.y - v2.y) * (vl.y - v2.y));

b = sqrt((v2.x - v3.x) * (v2.x - v3.x) + (v2.y - v3.y) * (v2.y - v3.y));

с = sqrt((vl.x - v3.x) * (vl.x - v3.x) + (vl.y - v3.y) * (vl.y - v3.y));

cout « "Constructor_1 for: " « objID « " (" « name « ")" « endl;  // отладочный вывод

}

// Конструктор пустого (нулевого) треугольника Triangle::Triangle(const char* ident)

{

char buf[16];

objID = new char[strlen(ident) +1];

strcpy(objID. ident);

count++;

sprintf(buf, "Треугольник %d", count);

name = new char[strlen(buf) +1];

strcpy(name, buf);

a = b = с = 0;

cout « "Constructor_2 for: " « objID « " (" « name « ")" « endl;  // отладочный вывод 

}

// Деструктор 

Triangle::~Triangle()

{

cout « "Destructor for: " « objID « endl;

delete [] objID;

delete [] name;

}      

// Показать объект

void Triangle::Show() const

{

cout « name «":";

vl.Show():  v2.Show():  v3.Show();

cout « endl;

    }

// Показать стороны и площадь объекта

void Triangle::ShowSideAndArea() const

{

double p = (a + b + c) / 2;

double s = sqrt(p * (p - a) * (p - b) * (p - c);

cout « " " « endl;

cout « name « ":";

cout.precision(4);

cout « " a= " « setw(5) «a;

cout « ", b= " « setw(5) « b;

cout « ", c= " « setw(5) « c;

cout « ":\ts= " « s « endl;

}

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

// Main.cpp

#include <iostream>

#include "Triangle.h"

//#include "CyrIOS.h" // for Visual C++ 6.0

using namespace std;

int    Menu();

int   GetNumber(int,  int);

void ExitBack();

void Show(Triangle* [],  int);

void Move(Triangle* [],  int);

void FindMax(Triangle* [],  int);

void IsIncluded(Triangle* [],  int);

// Инициализация глобальных переменных

int Triangle::count =0;

//   главная функция

int main()

{

// Определения точек

Point pl(0,  0); Point p2(0.5,   1);

Point p3(l,  0); Point p4(0, 4.5);

Point p5(2.   1);    Point p6(2.  0);

Point p7(2, 2); Point p8(3,  0);

// Определения треугольников

Triangle triaA(pl, p2, рЗ, "triaA");

Triangle triaB(pl. p4, p8, "triaB");

Triangle triaC(pl. p5. p6. "triaC");

Triangle triaD(pl. p7. p8. "triaD");

// Определение массива указателей на треугольники

Triangle* pTria[] = { &triaA, &triaB. &triaC, &triaD }; int n = sizeof (pTria) / sizeof (pTria[0]);

// Главный цикл

bool done = false: whfie (!done)

{

switch (Menu())

{

case 1: Show(pTria, n);      break:

case 2: Move(pTria, n);      break;

case 3: FindMax(pTria, n);    break;

case 4: IsIncluded(pTria, n);  break;

case 5: cout « "Конец работы." « endl;

done = true;  break;

}

}

return 0;

}

II   вывод меню

int Menu()

{

cout « "\n===== Главное  меню =====" « endl;

cout « "1 - вывести все объекты\t 3 - найти максимальный" « endl;

cout « "2 - переместить\t\t 4 - определить отношение включения" « endl;

cout « "\t\t  5 - выход" « endl;

return GetNumber(1, 5);

}

//   ввод целого числа в заданном диапазоне

int GetNumber(int min, int max)

{

int number = min - 1; while (trye)

{

cin » number;

if ((number >= min) && (number <= max) && (cin.peek() == '\n'));

break;

else 

{

cout « "Повторите ввод (ожидается число от " « min « " до " « max « "):" « endl; cin.clear();

while (cin.get() != '\n') {};

}

}

return number;

}

// возврат в функцию с основным меню

void ExitBack()

{

cout « "Нажмите Enter." « endl;

cin.get(); cin.get();

}

//   вывод всех треугольников

void Show(Triangle* p_tria[]. int k)

{

cout « "=== Перечень треугольников ===" « endl;

for (int i = 0; i < k: ++i) p_tria[i]->Show();

for (i = 0; i < k; ++i)

p_tria[i]->ShowSideAndArea();

ExitBack();

II перемещение

void Move(Triangle* p_tria[], int k)

{

cout « "======= Перемещение ======" « endl;

// здесь будет код функции...

ExitBack();

}

//   поиск максимального треугольника

void FindMax(Triangle* p_tria[], int k)

{

cout « "= Поиск максимального треугольника =" « endl;

// здесь будет код функции...

ExitBack();

}

//   определение отношения включения

void Islncluded(Triangle* p_tria[], int k)

{

cout « "===== Отношение включения =====" « endl;

// здесь будет код функции...

ExitBack();

}

//    конец проекта Task1_2    

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

Рекомендуем вам обратить внимание на следующие моменты в проекте Task1_2.

1. Класс Point (файлы Point.h, Point.cpp).

Реализация класса Point пока что содержит единственный метод Show(), назначение которого очевидно: показать объект типа Point на экране. Здесь следует заметить, что при решении реальных задач в какой-либо графической оболочке метод Show() действительно нарисовал бы нашу точку, да еще в цвете. Но мы-то изучаем «чистый» C++, так что придется удовольствоваться текстовым выводом на экран основных атрибутов точки - ее координат.

2. Класс Triangle (файлы Triangle.h, Triangle.cpp).

 Назначение большинства полей и методов очевидно из их имен и комментариев.

Поле static int count играет роль глобального счетчика создаваемых объ ектов; мы сочли удобным в конструкторах генерировать имена треугольни ков автоматически: «Треугольник 1», «Треугольник 2» и т. д., используя текущее значение count (возможны и другие способы именования тре угольников).

Поле char* objID избыточно для решения нашей задачи - оно введено исключительно для целей отладки и обучения; вскоре вы увидите, что благодаря отладочным операторам печати в конструкторах и деструкторе удобно наблюдать за созданием и уничтожением объектов.

Метод ShowSideAndArea() введен также только для целей отладки, - убедившись, что стороны треугольника и его площадь вычисляются правильно (с помощью калькулятора), в дальнейшем этот метод можно удалить.

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

Метод Show() - см. комментарий выше по поводу метода Show() в классе Point. К сожалению, здесь нам тоже не удастся нарисовать треугольник на экране; вместо этого печатаются координаты его вершин.

3.  Основной модуль (файл Main.cpp).

Инициализация глобальных переменных: обратите внимание на оператор int Triangle::count = 0: - если вы забудете это написать, компилятор очень сильно обидится.

Функция main ():

- определения восьми точек p1,..., р8 выбраны произвольно, но так, чтобы из них можно было составить треугольники;

- определения четырех треугольников сделаны тоже произвольно, впоследствии на них будут демонстрироваться основные методы класса; однако не забывайте, что вершины в каждом треугольнике должны перечисляться по часовой стрелке;

- далее определяются массив указателей Triangle* pTria[] с адресами объявленных выше треугольников и его размер n; в таком виде удобно передавать адрес pTria и величину n в вызываемые серверные функции;

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

Функция Menu () - после вывода на экран списка пунктов меню вызывается функция GetNumber(), возвращающая номер пункта, введенный пользователем с клавиатуры.

Функция Show() пpocто выводит на экран перечень всех треугольников. В завершение вызывается функция ExitBack(), которая обеспечивает заключительный диалог с пользователем после обработки очередного пункта меню.

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

Тестирование и отладка первой версии программы

После компиляции и запуска программы вы должны увидеть на экране следующий текст:

Constructor_1 for: triaA (Треугольник 1)

Constructor_1 for: tnaB (Треугольник 2)

Constructor_1 for: triaC (Треугольник 3)

Constructor_1 for: triaD (Треугольник 4)

=============== Главное  меню===============

  1.  - вывести все объекты 3 - найти максимальный       
  2.  - переместить    4 - определить отношение включения

5 - выход

Введите с клавиатуры цифру 12. Программа выведет:

1
======= Перечень треугольников ========

Треугольник 1: (0. 0) (0.5.  1)    (1, 0)

Треугольник 2: (0, 0) (0.  4.5)    (3.  0)

Треугольник 3: (0. 0) (2.  1)    (2.  0)

Треугольник 4: (0. 0) (2. 2)    (3,  0)

Треугольник 1:  а=1.118.  b=1.118, с= 1:  s=0.5

Треугольник 2:  а=4.5,  b=5.408, с=3: s= 6.75

Треугольник 3:  а=2.236,  b=1, с=2:  s= 1

Треугольник 4: а=2.828, b=2.236, с=3:  s=3

Нажмите Enter.

После ввода числовой информации всегда подразумевается нажатие клавиши Enter. Выбор первого пункта меню проверен. Нажмите Enter. Программа выведет:

=========== Главное  меню ===========

Теперь проверим выбор второго пункта меню. Введите с клавиатуры цифру 2. На экране должно появиться:

2

================== Перемещение ===============

Нажмите Enter.

Выбор второго пункта проверен. Нажмите Enter. Программа выведет:

============ Главное  меню =============

Теперь проверим ввод ошибочного символа. Введите с клавиатуры любой буквенный символ, например w, и нажмите Enter. Программа должна выругаться:

Повторите ввод (ожидается число от 1 до 5):

Проверяем завершение работы. Введите цифру 5. Программа выведет:

5

Конец работы.

Destructor for: triaD

Destructor for: triaC

Destructor for: triaB

Destructor for: triaA

Тестирование закончено. Обратите внимание на то, что деструкторы объектов вызываются в порядке, обратном вызову конструкторов.

Продолжим разработку программы. На втором этапе мы добавим в классы Point и Triangle методы, обеспечивающие перемещение треугольников, а в основной модуль - реализацию функции Move(). Кроме этого, в классе Triangle мы удалим метод ShowSideAndArea(), поскольку он был введен только для целей отладки и свою роль уже выполнил.

Этап 2

Внесите следующие изменения в тексты модулей проекта.

1. Модуль Point.h: добавьте сразу после объявления метода Show() объявление операции-функции «+=», которая позволит нам впоследствии реализовать метод перемещения Move() в классе Triangle:

void operator +=(Point&);

2. Модуль Point.cpp. Добавьте код реализации данной функции:

void Point::operator +=(Point& p)

{

x += p.x:       у += р.у:

}

3. Модуль Triangle.h.

Удалите объявление метода ShowSideAndArea().  

Добавьте объявление метода:

void Move(Point);

4. Модуль Triangle.cpp.  Удалите метод ShowSideAndArea().

 Добавьте код метода Move():

// Переместить объект на величину (dp.x. dp.у) void Triangle: :Move(Point dp)

{

vl += dp;        

v2 += dp;        

v3 += dp;

}

5. Модуль Main.cpp.

В список прототипов функций в начале файла добавьте сигнатуру:

double GetDouble();

Добавьте в файл текст новой функции GetDouble() либо сразу после функции Show(), либо в конец файла. Эта функция предназначена для ввода вещественного числа и вызывается из функции Move(). В ней предусмотрена защита от ввода недопустимых (например, буквенных) символов аналогично тому, как это решено в функции GetNumber():

//   ввод вещественного числа

double GetDouble()

{

double value; while (true)  

{

cin » value;

if (cin.peek() == '\n') break;

else

{

cout « "Повторите ввод (ожидается вещественное число):" « endl; cin.clear();

while (cin.get()  != '\n') {};

}

}

return value;

} 

Замените заглушку функции Move() следующим кодом:

//   перемещение

void Move(Triangle* p_tria[].  int k)

{

cout « "========= Перемещение =========" « endl;

cout « "Введите номер треугольника (от 1 до " « к « "):  ";

int i = GetNumber(d, k) - 1;

p_tria[i]->Show();

Point dp;

cout « "Введите смещение по х: ";

dp.x = GetDouble(): cout « "Введите смещение по у: ";

dp.у = GetDouble();

p_tria[i]->Move(dp);

cout « "Новое положение треугольника:" « endl;

p_tria[i]->Show();

ExitBack();

} 

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

2    

============= Перемещение =============

Введите номер треугольника (от 1 до 4): 1

Треугольник 1: (0. 0) (0.5. 1) (1, 0)

Введите смещение по х: 2.5

Введите смещение по у: -7

Новое положение треугольника:

Треугольник 1: (2.5, -7) (3, -6) (3.5, -7)

Нажмите Enter.

Продолжим разработку программы.

Этап 3

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

1. Модуль Triangle. h: добавьте объявление функции-операции:

bool operator >(const Triangle&) const;

2. Модуль Triangle.cpp: добавьте код реализации функции-операции:

// Сравнить объект (по площади) с объектом tria

bool Triangle:: operator >(const Triangles tria) const

{

double p = (a + b + c) / 2;

double s = sqrt(p * (p - a) * (p - b) * (p - c));

double p1 = (tria a + tria b + tria c) / 2;

double s1 = sqrt(pl * (p1 - tria.a) * (p1 - tria.b) * (pi - tria.c));

if (s > s1) return true;

else

return false;

}

3. Модуль Main.cpp: замените заглушку функции FindMax() следующим кодом:

//   поиск максимального треугольника

void FindMaxCTriangle* p_tria[]. int k)

{

cout « "= Поиск максимального треугольника =" « endl;

// Создаем объект triaMax, который по завершении поиска будет

// идентичен максимальному объекту.

// Инициализируем его значением 1-го объекта из массива объектов.

Triangle triaMax("triaMax"):

triaMax = *p_tria[0];

// Поиск

for (int i = 1:" i < 4: ++i)  if (*p_tria[i] > triaMax) triaMax = *p_tria[i];

cout « "Максимальный треугольник: " « triaMax.GetName() « endl;

ExitBack();   

}

Откомпилируйте программу и запустите. Выберите третий пункт меню. На экране должно появиться:

3

=== Поиск максимального треугольника ==

Constructor_2 for: triaMax (Треугольник 5)

Максимальный треугольник: Треугольник 2

Нажмите Enter.

Как видите, максимальный треугольник найден правильно. Нажмите Enter. Появится текст:

Destructor for: triaB

============ Главное     меню==============

  1.  - вывести все объекты     3 - найти максимальный
  2.  - переместить 4 - определить отношение включения

5 - выход

Чтобы завершить работу, введите цифру 5 и нажмите Enter. На передний план выскочит диалоговая панель с жирным белым крестом на красном кружочке и с сообщением об ошибке:

Debug Assertion Failed!

Program:  C:\.. .   .  MAIN.EXE File: dbgdel.cpp Line 47

Программа выведет на экран следующее:

5

Конец работы.

Destructor for: triaD

Destructor for: triaC

Destructor for:  ННННННННННээээЭЭЭЭЭЭЭЭЭЭО

В предыдущем выводе нашей программы после того как функция FindMax() выполнила основную работу и вывела на экран сообщение

Максимальный треугольник: Треугольник 2

программа пригласила пользователя нажать клавишу Enter. Это приглашение выводится функцией ExitBack(). А вот после нажатия клавиши Enter на экране появился текст:

Destructor for: triaB 

после которого опять было выведено главное меню.

Значит, деструктор для объекта triaB был вызван в момент возврата из функции FindMax(). Внутри функции объявлен объект triaMax, и мы даже видели работу его конструктора:

Constructor_2 for:  triaMax (Треугольник 5)

А где же вызов деструктора, который по идее должен произойти в момент возврата из функции FindMax()? Объект triaMax после своего объявления неоднократно модифицируется с помощью операции присваивания. Последняя такая модификация происходит в цикле, причем объекту triaMax присваивается значение объ-  екта triaB. А теперь давайте вспомним, что если мы не перегрузили операцию присваивания для некоторого класса, то компилятор сделает это за нас, но в такой «операции присваивания по умолчанию» будут поэлементно копироваться все поля объекта. При наличии же полей типа указателей возможны проблемы, что мы и получили.

В поля objID и name объекта triaMax были скопированы значения одноименных полей объекта triaB. В момент выхода из функции FindMax() деструктор объекта освободил память, на которую указывали эти поля. А при выходе из основной функции main() деструктор объекта triaB попытался еще раз освободить эту же память. Это делать нельзя, потому что этого делать нельзя никогда.

Нужно добавить в класс Triangle перегрузку операции присваивания, а заодно и конструктор копирования.

Внесите следующие изменения в тексты модулей проекта:

1. Модуль Triangle.h.

Добавьте объявление конструктора копирования:

Triangle(const Triangle&);   // конструктор копирования  

Добавьте объявление операции присваивания:     

Triangle& operator =(const Triangle&);

2. Модуль Triangle. cpp.

Добавьте реализацию конструктора копирования:

// Конструктор копирования

Triangle::Triangle(const Triangle& tria):  vl(tria.vl),  v2(tria.v2),

v3(tria.v3)

{

cout « "Copy constructor for: " « tria.objID « endl ; // отладочный вывод

objID = new char[strlen(tria.objID) + strlen(копия)") + 1];

strcpy(objID, tria.objID): strcat(objID,  "(копия)");

name = new char[strlen(tria.name) +1]: strcpy(name. tria.name); a = tria.a; b = tria.b; с = tria.с;

}

Добавьте реализацию операции присваивания:

// Присвоить значение объекта tria

Triangle& Triangle::operator =(const Triangle& tria)

{

cout « "Assign operator:  " « objID « " = " « tria.objID « endl; // отладочный вывод

if (&tria == this) return *this; delete [] name;

name = new char[strlen(tria.name) + 1]; strcpy(name, tria.name);

a = tria.a;    b = tria.b;    с = tria.с;

return *this;

}

И в конструкторе копирования, и в операторе присваивания перед копированием содержимого полей, на которые указывают поля типа char*, для них выделяется новая память. Обратите внимание, что в конструкторе копирования после переписи поля objID мы добавляем в конец этой строки текст «(копия)». А в операции присваивания это поле, идентифицирующее объект, вообще не затрагивается и остается в своем первоначальном значении. Все это полезно для целей отладки.

Откомпилируйте и запустите программу. Выберите третий пункт меню. На экране должен появиться текст:

3

=== Поиск максимального треугольника ==

Constructor_2 for: triaMax (Треугольник 5)

Assign operator: triaMax = triaA

Assign operator: triaMax = triaB

Максимальный треугольник: Треугольник 2

Нажмите Enter.

Обратите внимание на отладочный вывод операции присваивания. Продолжим тестирование. Нажмите Enter. Программа выведет:

Destructor for: triaMax

============== Главное     меню===============

  1.  - вывести все объекты    3 - найти максимальный
  2.  - переместить4 - определить отношение включения

5 - выход

Обратите внимание на то, что был вызван деструктор для объекта triaMax, а не triaB. Продолжим тестирование. Введите цифру 5. Программа выведет:

5

Конец работы.

Destructor for: triaD

Destructor for: triaC

Destructor for: triaB

Destructor for: triaA

Осталось теперь решить самую сложную подзадачу - определение отношения включения одного треугольника в другой.

Этап 4

Из многочисленных подходов к решению этой подзадачи наш выбор остановился на алгоритме, в основе которого лежит определение относительного положения точки и вектора на плоскости1. Вектор - это направленный отрезок прямой линии, начинающийся в точке beg_p и заканчивающийся в точке end_p. При графическом изображении конец вектора украшают стрелкой. Теперь призовите ваше пространственное воображение или вооружитесь карандашом и бумагой, чтобы проверить следующее утверждение. Вектор (beg_p, end_p) делит плоскость на пять непересекающихся областей:

  1.  все точки слева от воображаемой бесконечной прямой1, на которой лежит
    наш вектор (область
    LEFT),
  2.  все точки справа от воображаемой бесконечной прямой, на которой лежит наш вектор (область RIGHT),
  3.  все точки на воображаемом продолжении прямой назад от точки beg_p в бесконечность (область BEHIND),

4) все точки на воображаемом продолжении прямой вперед от точки end_p в бесконечность (область AHEAD),

5) все точки, принадлежащие самому вектору (область BETWEEN).

Для выяснения относительного положения точки, заданной некоторым объектом класса Point, добавим в класс Point перечисляемый тип:

enum ORIENT {  LEFT.  RIGHT.  AHEAD.  BEHIND,  BETWEEN }:

а также метод C1assify(beg_p, end_p), возвращающий значение типа ORIENT для данной точки относительно вектора (beg_p, end_p).

Обладая этим мощным методом, совсем нетрудно определить, находится ли точка внутри некоторого треугольника. Мы договорились перед началом решения задачи, что треугольники будут задаваться перечислением их вершин в порядке изображения их на плоскости по часовой стрелке. То есть каждая пара вершин образует вектор, и эти векторы следуют один за другим по часовой стрелке. При этом условии некоторая точка находится внутри треугольника тогда и только тогда, когда ее ориентация относительно каждой вектора-стороны треугольника имеет одно из двух значений: либо RIGHT, либо BETWEEN. Эту подзадачу будет решать метод InTriangle() в классе Point.

Изложим по порядку, какие изменения нужно внести в тексты модулей. 1.  Модуль Point.h.

Добавьте перед объявлением класса Point объявление нового типа ORIENT, а также упреждающее объявление типа Triangle,  чтобы имя типа Triangle было известно компилятору в данной единице трансляции, так как оно используется в сигнатуре метода InTriangle().:

enum ORIENT

{  

LEFT.  RIGHT.  AHEAD,  BEHIND,  BETWEEN

};

class Triangle;

Добавьте внутри класса Point объявления функций:

Point operator +(Point&);

Point operator -(Point&);

double Length() const; // определяет длину вектора точки в полярной системе координат

ORIENT Classify(Point&,  Point&) const;    // определяет положение точки относительно вектора, заданного двумя точками
   
bool  InTriangle(Triangle&) const; // определяет, находится ли точка внутри треугольника

Функция-операция «-» и метод Length() будут использованы при реализации метода Classify(), а функция-операция «+» добавлена для симметрии. Метод Classify(), в свою очередь, вызывается из метода InTriangle(). 1.  Модуль Point.cpp.

Добавьте после директивы #include <iostream> директиву #include <math.h>.

Она необходима для использования функции sqrt(x) из математической библиотеки C++ в алгоритме метода Length().

Добавьте после директивы #include «Point. h» директиву #include «Triangle. h».

 Последняя необходима в связи с использованием имени класса Triangle в данной единице трансляции.

Добавьте реализацию функций-операций:

Point Point::operator +(Point& p)  

{

return Point(x + p.x, у + р.у);

}

Point Point::operator -(Points p)  

{

return Point(x - p.x, у - p.у);

}

Добавьте реализацию метода Length():

double Point::Length() const

{

return sqrt(x*x + y*y);

}

Добавьте реализацию метода Classify():

ORIENT Point::Classify(Point& beg_p. Points end_p) const

{

Point p0 = *this; Point a = end_p - beg_p; Point b = p0 - beg_p; double sa = a.x * b.y - b.x * a.y;

if (sa > 0.0) return LEFT;

if (sa < 0.0) return RIGHT;

if ((a.x * b.x < 0.0)   ||   (a.y * b.y < 0.0)) return BEHIND;

if (a. Length О < b. Length()) return AHEAD;

return BETWEEN;

 } 

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

Добавьте реализацию метода InTriangle():

bool Point::InTriangle(Triangle& tria) const

{

ORIENT orl = Classify(tria.Get_vl(), tria.Get_v2O); ORIENT or2 = Classify(tria.Get_v2(), tria.Get_v3O): ORIENT огЗ = Classify(tria.Get_v3(), tria.Get_vl());

if ((orl == RIGHT || orl == BETWEEN)

&& (or2 == RIGHT || or2 == BETWEEN)

&& (оrЗ == RIGHT || оrЗ == BETWEEN)) return true;

else  return false;

}

2. Модуль Triangle.h: добавьте в классе Triangle объявление дружественной функции (все равно, в каком месте):

friend bool TriaInTna(Triangle, Triangle);  // Определить, входит ли один треугольник во второй

3. Модуль Triangle.cpp: добавьте в конец файла реализацию внешней дружественной функции:

// Определить,  входит ли треугольник trial в треугольник tria2

bool TriaInTria(Triangle trial, Triangle tria2)

{

Point v1 = trial.Get_v1();

Point v2 = trial.Get_v2();

Point v3 = trial.Get_v3();

return (vl.InTriangle(tria2) && v2.InTriangle(tria2) && v3.InTriangle(tria2));

return true;

}

Результат, возвращаемый функцией, основан на проверке вхождения каждой вершины первого треугольника (trial) во второй треугольник (tria2).

4. Модуль Main.cpp: замените заглушку функции IsIncluded() следующим кодом:

//   определение отношения включения

void IsIncluded(Triangle* p_tria[]. int k)

{

cout « "==== Отношение включения ====" « endl;

cout « "Введите номер 1-го треугольника (от 1 до " « к « "):  ";

int i1 = GetNumber(1.  к)  - 1;

cout « "Введите номер 2-го треугольника (от 1 до " « к «"):";

int i2 = GetNumber(1. к) - 1;

if (TriaInTria(*p_tria[il], *p_tria[i2]))

cout « p_tria[i1]->GetName() « "  - входит в - " « p_tria[i2]->GetName() « endl;

else

cout « p_tria[i1]->GetName() « " - не входит в - " « p_tria[i2]->GetName() « endl; ExitBack();

}

Модификация проекта завершена. Откомпилируйте и запустите программу. Выберите четвертый пункт меню. Следуя указаниям программы, введите номера сравниваемых треугольников, например 1 и 2. Вы должны получить следующий результат:

4

======== Отношение включения ==========

Введите номер 1-го треугольника (от 1 до 4):  1 Введите номер 2-го треугольника (от 1 до 4): 2 Copy constructor for: triaB Copy constructor for: triaA Destructor for: triaA(копия) Destructor for: triaB(копия) Треугольник 1 - входит в - Треугольник 2 Нажмите Enter.

                           

Аппаратура и материалы. Для выполнения лабораторной работы необходим персональный компьютер со следующими характеристиками: процессор Intel Pentium-совместимый с тактовой частотой 800 МГц и выше, оперативная память - не менее 64 Мбайт, свободное дисковое пространство - не менее 500 Мбайт, устройство для чтения компакт-дисков, монитор типа  Super VGA (число цветов от 256) с диагональю не менее 15. Программное обеспечение - операционная система Windows2000/XP  и выше, среда разработки приложений Microsoft Visual Studio.

Указания по технике безопасности. Техника безопасности при выполнении лабораторной работы совпадает с общепринятой для пользователей персональных компьютеров, самостоятельно не производить ремонт персонального компьютера, установку и удаление программного обеспечения;  в случае неисправности персонального компьютера сообщить об этом обслуживающему персоналу лаборатории (оператору, администратору); соблюдать правила техники безопасности при работе с электрооборудованием; не касаться электрических розеток металлическими предметами; рабочее место пользователя персонального компьютера должно содержаться в чистоте; не разрешается возле персонального компьютера принимать пищу, напитки.

Методика и порядок выполнения работы. Перед выполнением лабораторной работы каждый студент получает индивидуальное задание. Защита лабораторной работы происходит только после его выполнения (индивидуального задания). При защите лабораторной работы студент отвечает на контрольные вопросы, приведенные в конце, и поясняет выполненное индивидуальное задание. Ход защиты лабораторной работы контролируется преподавателем. Порядок выполнения работы:

1. Проработать примеры, приведенные в лабораторной работе.

2. Реализовать класс Time.

3. Реализовать класс треугольников.

4. Решить задачу на двумерные массивы согласно своему варианту лабораторной работы №6 с использованием классов.

5. Реализовать класс многочленов и смоделировать операции над ними, принимая во внимание то, что многочлен – это объект, имеющий свойства: коэффициенты и степени.

Содержание отчета и его форма. Отчет по лабораторной работе должен состоять из:

1. Названия лабораторной работы.

2. Цели и содержания лабораторной работы.

3. Ответов на контрольные вопросы лабораторной работы.

4. Формулировки заданий и порядка их выполнения.

Отчет о выполнении лабораторной работы в письменном виде сдается преподавателю.

Вопросы для защиты работы

1. Какова структура программы на объектно-ориентированном языке?

2. Что такое объект ООП?

3. Что такое класс?

4. На какие разделы может делиться класс?

5. Назовите и охарактеризуйте три основных принципа в использовании классов.

6. Что такое конструктор и для чего он используется?

7. Как происходит индексирование объектов класса?

8. Как происходит вызов функции?

9. Что такое композиция классов?

10. Для чего и каким образом используются дружественные функции?

11. Для чего требуется указатель this?

12. Для чего используется модификатор static?

Список рекомендуемой литературы

а) Основная литература:

  1.  Подбельский В.В. Язык Си++: Учеб. пособие. 5-е изд. - М.: Финансы и статистика, 2001.
  2.  Павловская Т.А. C/C++. Программирование на языке высокого уровня - СПб.: Питер, 2005.
  3.  C/C++. Структурное программирование: Практикум / Т.А. Павловская, Ю.А. Щупак - СПб.: Питер, 2005.
  4.  Павловская Т.А., Щупак Ю.А. C++. Объектно-ориентированное программирование: Практикум. - СПб.: Питер, 2005.
  5.  Культин Н. C++ Builder. - СПб.: БХВ-Петербург, 2004.
  6.  Шамис В. Borland C++ Builder 6. - СПб.: Изд-во Питер, 2004.
  7.  Послед Б.С. C++ Borland Builder 6. - М.: Диасофт, 2003.
  8.  Шилдт Г. Самоучитель С++: Пер. с англ. 3-е изд. - СПб.: БХВ-Петербург, 2001.

б) Дополнительная литература:

  1.  Страуструп Б. Язык программирования Си++: Пер. с англ. - М.: Радио и связь, 1991.
  2.  Эллис М., Страуструп Б. Справочное руководство по языку программирования С++ с комментариями. Проект стандарта ANSI: Пер. с англ. - М.: Мир, 1992.
  3.  Буч Г. Объектно-ориентированное проектирование с примерами применения: Пер. с англ. - М.: Конкорд, 1992.
  4.  Буч Г. Объектно-ориентированный анализ и проектирование с примерами приложений на C++, 2-е изд. / Пер. с англ. - М.: «Изд-во Бином», 1999.
  5.  Сурков К.А. Сурков Д.А. Вальвачев А.Н. Программирование в среде C++ Builder. - Мн.: ООО «Попурри», 1998.


Выражение

Выражение

Оператор 1

Оператор 2

Оператор

Выражение

Операторы 1

Операторы 2

Операторы n

Операторы

Начальные

установки

Инициализация

Выражение

Модификация

Операторы

Начальные

установки

Выражение

Операторы

Модификация

параметров

цикла

Модификация

параметров

цикла

Операторы

Выражение

Начальные

установки

      

                 …          …

 

                             

                             …  

                                 

                                  




1. тема физических упражнений состоит из трех видов- Ациклические упражнения способствующие развитию вынослив
2. Рациональное использование ферментной диагностики при инфаркте миокарда
3. Вклад А
4. Реферат- Задачи и принципы лечебного питания
5. Город Муром во второй половине 20 века
6. Строковый тип данных в языке Pascal
7. Основные проявления заболевания ~ атрофия скелетных мышц фасцикуляции спастичность гиперрефлексия пато
8. Понимание деятельности аудируемого лица среды в которой она осуществляется и оценка рисков существенного
9. Базовое понятие ldquo;цивилизованногоrdquo; было развито в XVII веке французскими философами в рамках бинарного
10. Функции фараона в Древнем Египте
11. ЛЕКЦИЯ 6 ЛИТЕРАТУРНЫЕ НАПРАВЛЕНИЯ XVII XVIII ВЕКА 1
12. Псковский сельскохозяйственный техникум по воспитательной работе Симаева Н
13.  Обобщенный закон Гука для ортотропного слоистого композиционного материала
14. Теоретичні основи розвитку творчих здібностей учнів в позашкільний час
15. 12 1 Философия позитивизма
16. Бал прессы. 2013 г
17. Тема 5 россия во второй половине XVII века Доклад 1 Политика протекционизма и меркантилизма в России
18. Мировоззренческие портреты Ключевский ВО
19. Введение1
20. Рак молочної залози