5709

Программирование на языках среднего уровня С/С++

Конспект

Информатика, кибернетика и программирование

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

Русский

2012-12-18

689 KB

30 чел.

Предисловие

Настоящий конспект лекций посвящен программированию на языках среднего уровня С/С++, в нем рассмотрен объектно-ориентированный подход программирования.

Условно конспект лекций можно разделить на две части: первая часть посвящена основным теоретическим принципам профессиональной разработки программного обеспечения и методам его проектирования; во второй части освещены вопросы практической реализации проектов на языках программирования С/С++ и Visual C++, рассмотрены идеология, состав языка программирования, структуры программ, главная цель настоящего издания – дать общий подход к разработке программного обеспечения на языках программирования С/С++ и Visual C++, прежде всего, с точки зрения алгоритмических языков, на основе которых возможно углубленное изучение данной дисциплины.

Конспект лекций предназначен для студентов вузов следующих специальностей:

«Инженерное дело в медико-биологической практике»; «Управление качеством»; «Радиотехника»; «Радиосвязь, радиовещание и телевидение»; «Бытовая радиоэлектронная аппаратура»; «Проектирование и технология электронно-вычислительных средств»; «Радиотехника».

Авторы выражают благодарность рецензентам кандидатам технических наук Л. Г. Нехорошковой и С. П. Зыкову за ценные замечания.

ВВЕДЕНИЕ

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

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

Языки программирования С/С++ являются универсальными языками программирования среднего уровня, наиболее широко распространенными и часто используемыми в мире. Они являются основными языками при разработке системного и прикладного программного обеспечения для процессоров цифровой обработки сигналов, то есть, языками промышленной разработки. Владение ими является необходимым условием программирования на профессиональном уровне.

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

Лекция 1.

Введение в программирование

на языках С/C++

1.1. Предисловие к курсу

Курс “Информатика“ является одним из основных в русле подготовки специалистов по информационным системам. Разработка программного обеспечения и его сопровождение было и остается важнейшей функцией специалистов в области микропроцессорных, компьютерных систем и систем управления базами данных (СУБД). Широкое внедрение вычислительных машин во все сферы промышленности, связи, систем управления и документооборота требует массу программного обеспечения непрерывно возрастающей сложности. Еще недавно программирование считалось искусством, теперь - специальностью, работой как отдельных личностей, так и больших коллективов.

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

1.2. Идеология языка

Языки С и С++ являются наиболее широко распространенными и часто используемыми языками программирования в мире. Они являются основными языками при разработке как системного, так и прикладного программного обеспечения, то есть, языками промышленной разработки. Владение ими является необходимым условием получения высокооплачиваемой работы в области информационных технологий. Язык С появился в 1972 г. благодаря усилиям двух специалистов лаборатории – Бейла Брайена Кернигана и Денниса Ритчи и быстро завоевал признание среди разработчиков всего мира. Этому способствовали его следующие характерные особенности:

Эффективность. Программы, написанные на С, обладают небольшим размером и высокой скоростью исполнения.

Лаконичность. Запись алгоритма выразительна и кратка.

Компактность. Язык содержит мало встроенных средств и ключевых слов.

Мощность. Мощность применения достигается за счет использования многочисленных библиотек.

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

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

В 1981 году появился язык С++, который многие называют “С с классами“. Создатель С++ Бъярн Страуструп ввел в традиционный С средства объектного языка: классы для представления внутренней структуры объектов, механизм наследования и новые средства работы с динамической памятью и с вводом/выводом. Данные средства позволили не только писать объектно-ориентированные программы, но и создавать библиотеки шаблонов, классов, то есть расширить инструментарий программиста и сферу применения языка. В дальнейшем мы будем рассматривать С и С++ как единый язык, кроме случаев, когда необходимо подчеркнуть разницу между ними. Отметим только, что С/С++ сочетает в себе вышеперечисленные достоинства С и новые возможности С++, позволяя “изнутри“ рассмотреть принципы высокоуровневого программирования.

Язык С++ объединяет в себе средства высокоуровневого и низкоуровневого программирования. К первым можно отнести структуры, классы, механизмы наследования и позднего связывания, шаблоны.

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

1.3. Обзор среды Microsoft Developer Studio

Студия разработчика фирмы Microsoft (Microsoft Developer Studio) – это интегрированная среда для разработки, позволяющая функционировать различным средам разработки, одна из которых Visual C++, другая – Visual J++. В дальнейшем будет идти речь только о среде разработки Visual C++.

В среде Visual C++ можно строить различные типы проектов. Такие проекты после их создания можно компилировать и запускать на исполнение. Рассмотрим некоторые типы проектов, которые можно создавать при помощи различных средств (мастеров проектов) Microsoft Visual C++:

MFC AppWizard (exe) при помощи мастера приложений можно создать проект Windows-приложения. Он имеет однодокументный, многодокументный или диалоговый интерфейс. Однодокументное приложение может предоставлять пользователю в любой момент времени возможность работать только с одним файлом. Многодокументное приложение, напротив, может одновременно представлять несколько документов, каждый в собственном окне. Пользовательский интерфейс диалогового приложения представляет собой единственное диалоговое окно.

MFC AppWizard (dll) – этот мастер приложений позволяет создать структуру DLL, основанную на MFC. При помощи него можно определить характеристики будущей DLL.

AppWizard ATL COM – это средство позволяет создать элемент управления ActiveX или сервер автоматизации, используя новую библиотеку шаблонов ActiveX (ActiveX Template Library - ATL). Опции этого мастера дают возможность выбрать активный сервер (DLL) или исполняемый внешний сервер (exe-файл).

Custom AppWizard – при помощи этого средства можно создать пользовательские мастера AppWizard. Пользовательский мастер может базироваться на стандартных мастерах для приложений MFC или DLL, а также на существующих проектах или содержать только определяемые разработчиком шаги.

DevStudio Add-in Wizard – мастер дополнений позволяет создавать дополнения к Visual Studio. Библиотека DLL расширений может поддерживать панели инструментов и реагировать на события Visual Studio.

MFC ActiveX ControlWizard – мастер элементов управления реализует процесс создания проекта, содержащего один или несколько элементов управления ActiveX, основанных на элементах управления MFC.

Win32 Application – этот мастер позволяет создать проект обычного Windows-приложения. Проект создается незаполненным, файлы с исходным кодом в него следует добавлять вручную.

Win32 Console Application – мастер создания проекта консольного приложения. Консольная приложение – это программа, которая выполняется из командной строки окна DOS или Windows и не имеет графического интерфейса (окон). Проект консольного приложения создается пустым, предполагая добавление файлов исходного текста вручную.

Win32 Dynamic-Link Library – создание пустого проекта динамически подключаемой библиотеки. Установки компилятора и компоновщика будут настроены на создание DLL. Исходные файлы следует добавлять вручную.

Win32 Static Library – это средство создает пустой проект, предназначенный для генерации статической (объектной) библиотеки. Файлы с исходным кодом в него следует добавлять вручную. Благодаря этому можно говорить о двухэтапной технологии создания проектов. Вначале создается некая заготовка проекта с общими свойствами, подходящими для многих проектов этого типа. На втором этапе производится уже настройка, учитывающая специфику задачи. Для каждого этапа фирма Microsoft разработала свое инструментальное средство.

Использование средств разработки. В состав компилятора Microsoft Developer Studio встроены средства, позволяющие программисту облегчить разработку приложений. В первую очередь к ним относятся MFC AppWisard, ClassWizard и редактор ресурсов.

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

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

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

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

1.4. Жизненный цикл программного обеспечения

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

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

Этап проектирования можно разбить на более мелкие этапы:

- анализ технического задания;

- определение структур данных;

- разработка или адаптация алгоритмов обработки данных;

- структурная декомпозиция (разбиение исходной задачи на относительно независимые мелкие подзадачи);

- описание программы в виде схем, блок-схем или другим способом;

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

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

Этап кодирования – реализация структурной схемы программы на выбранном языке программирования и получение исполняемых модулей.

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

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

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

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

1.5. Общая структура программы

Язык С и С++ часто называют языком среднего уровня. Это означает, что он объединяет лучшие свойства языков высокого уровня, возможности и гибкость языка ассемблера.

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

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

Общая структура одномодульной программы, написанной на языке С и С++ (текст программы в одном файле), имеет следующий вид:

//Область директив препроцессора (include, define, …)

#include   <iostream.h>

//Объявления глобальных типов, переменных и констант

//Функции

Function 1

{

//Объявления локальных типов, переменных и констант

//Операторы

}

Function N

{

//Объявления локальных типов, переменных и констант

//Операторы

}

//Главная функция программы

void main ()

{

//Объявления локальных типов, переменных и констант

//Операторы

}

Программы обычно начинаются с директив препроцессора (начинаются с символа "#"), которые, по сути, не являются конструкциями языка С и обрабатываются до фактической компиляции программы. Их смысл – подстановка некоторого кода в программу. Так, к примеру, очень часто используется директива #include, которая включает в файл с исходным кодом программы текст внешнего заголовочного файла (с расширением .h). Заголовочные файлы содержат определения глобальных типов, констант, переменных и функций.

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

Традиционно программы на языке С используют расширение *.с, как программу на языке С, а программу, записанную в файл с расширением *.срр, – как программу на языке С++.

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

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

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

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

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

1.6. Директивы препроцессора

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

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

В 1989 году появился стандарт языка С (ANSI C). Этот документ определил, какие библиотеки следует считать стандартными и включать в любой пакет разработчика. Стандартные библиотеки обеспечивают доступ к важнейшим системным функциям, минуя системозависимые аспекты. Так, например, математическая функция sin () вычисляет синус вещественного числа, скрывая от пользователя особенности работы конкретного процессора.

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

Основные стандартные библиотеки С/С++ приведены в табл. 1.

Таблица 1

Основные стандартные библиотеки С/С++

Название

Назначение и содержание

1

2

math.h

математические функции.

ctype.h

библиотека символьных функций

string.h

библиотека строковых функций

iostream.h

операторы стандартного ввода/вывода

stdio.h 

функции стандартного ввода/вывода

stdlib.h 

библиотека функций для работы с динамической памятью

alloc.h 

работа со случайными числами, преобразования типов

conio.h 

работа с терминалом

time.h

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

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

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

Директива #include. Директива #include, которая используется фактически во всех программах для включения в текст программы заголовочных файлов (с определениями), имеющий расширение .h, или файлов .с (не содержащих функцию main ()).

Эта директива может использоваться в двух формах:

#include <имя файла>

или

#include "имя_файла"

Если имя файла заключено между знаками "<" и ">", то он считается частью стандартной библиотеки, поставляемой вместе с компилятором. Если же имя файла заключено в двойные кавычки, то считается, что он расположен в той же папке, что и файл с исходным кодом.

Директива #define. Директива #define указывает препроцессору на необходимость выполнить подстановку в тексте программы определенной последовательности символов другой последовательностью. Формат директивы:

#define заменяемая_последовательность фрагмент_подстановки

Например:

#define MyName "John Smith"

#define Condition (a > b)

#define Operation a = b

char str[] = MyName; //Равнозначно char str[] = "John Smith";

int a, b;

if Condition Operatrion; //Равнозначно if (a > b) a = b;

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

Директивы условной компиляции. Некоторые задачи отличаются лишь некоторыми параметрами, это позволяет создавать на языке С универсальный программный код. Однако для этого следует каким-то образом заменить те параметры, которые отличаются. Для таких целей используются директивы условной компиляции #ifdef, #ifndef, #else и #endif, а также #if и #elif.

Синтаксис для директивы #ifdef:

#ifdef имя_макроса

последовательность_операторов_1

#else

последовательноеть_операторов_2

#endif

Если имя макроса определено в программе, то компилируется первая последовательность операторов, в противном случае вторая последовательность (ветка #else может и отсутствовать).

Синтаксис для директивы #ifndef:

#ifndef имя_макроса

последовательность_операторов_1

#else

последовательность_операторов_2

#endif

В данном случае, в отличие от директивы #ifdef, первая последовательность операторов выполняется в том случае, если имя макроса в программе не определено.

Для условной компиляции можно также воспользоваться директивами #if, #elif. Их синтаксис:

#if выражение1

последовательность_операторов_1

#elif выражение2

последовательность_операторов_2

#else

последовательность_операторов_3

#endif

Эта конструкция работает аналогично условному оператору if-else. Компилятор оценивает выражения после #if и #elif до тех пор, пока одно из них не даст в результате TRUE, после чего в текст программы подставляется соответствующая последовательность операторов. Если оба выражения дают FALSE, то подставляется последовательность операторов после директивы #else (если она присутствует).

1.7. Построение исполняемого файла

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

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

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

Компилятор производит перевод текста на С++ в машинный код и создает объектный файл, который пока еще не готов к исполнению.

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

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

Текст программы помещается в один или несколько исходных файлов, по традиции имеющих расширение *.с или *.срр.

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

1.8. Строительные блоки программы

Объекты разработки программы строятся и проектируются, как совокупность частей, согласованная работа которых и реализует заданные функциональные возможности. Традиционно выделяют 5 уровней исходного текста программы, составляющих в целом АРХИТЕКТУРУ ПО:

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

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

Например, чтение строк с клавиатуры до появления пустой осуществляется посредством:

char buf[80]; while(*gets(buf));

цикл построкового чтения файла:

while(fgets(buf,255,fp)).

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

Процедуры (функции). Основным структурным элементом языка С является функция. Функция представляет собой исчерпывающее решение какой-либо небольшой задачи и представляет собой в С/C++ структурную единицу программы. Любая программа на C/С++ состоит минимум из одной функции (main), а максимальное их количество неограниченно. Оптимальный размер тела функции составляет 5-20 строк, по одному – два выражения в каждой. Однажды написанная и протестированная функция может многократно использоваться в пределах одной программы при разных наборах входных данных, осуществляя их обработку по некоторому алгоритму. Функция может считаться относительно независимой частью программы и переноситься в другие проекты по разработке ПО.

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

Библиотеки функций и классов. Наборы функций и иерархии классов образуют библиотеки. Содержимое библиотек должно быть универсально для решения широкого круга задач. Например, существуют библиотеки для работы с графикой (OpenGL, DirectX), со временем для обеспечения функций связи. Существующие библиотеки классов (Microsoft MFC, Borland OWL) облегчают создание Windows-приложений. Работа с текстом программы на последних двух уровнях требует от разработчика развитого абстрактного мышления и, как профессиональная деятельность, оценивается очень высоко.

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

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

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

Рассмотрим определения основных составляющих языка:

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

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

ключевое слово - зарезервированная последовательность символов в программе; основные ключевые слова представлены в табл. 2, расширенные ключевые слова в табл. 3;

Таблица 2

Ключевые слова подмножества С языка С++

auto

double

int

struct

break

else

long

switch

case

enum

register

typedef

char

extern

return

union

const

float

short

unsigned

continue

for

signed

void

default

goto

sizeof

volatile

do

if

static

while

Таблица 3

Расширенные ключевые слова

asm

_cs

_ds

_es

_ss

cdecl

far

huge

interrupt

near

pascal

литерал - символ или последовательность символов, обозначающих данные (символьные или числовые);

выражение - набор литералов, идентификаторов и символов операций, предназначенный для записи правила вычисления значения или элементарного действия в программе;

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

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

Контрольные вопросы

1. Какие методы проектирования ПО вы знаете?

2. Перечислите характерные особенности языка С/С++.

3. Приведите основные блоки программы С/С++.

4. Построение исполняемого файла С/С++.

5. Состав языка С/С++.

6. Общая структура программы на языке С/С++.

7. Директивы препроцессора.

8. Какие вы знаете жизненные циклы программного обеспечения?


Лекция 2.

Типы данных. переменные. Массивы.

операции и Указатели

2.1. Организация данных в С/С++

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

численные знаковые целые (int, short, char);

численные знаковые дробные (float, double, long (в С), long double (в С);

численные беззнаковые - все перечисленные выше типы с добавлением Unsigned;

сhar так же может использоваться, как символьный тип.

Кроме перечисленных типов данных в С++ добавляется еще два – это bool и wchar_t.

Язык С\С++ позволяет создавать пользовательские типы данных, которые определяются самим пользователем.

Стандартные типы и размеры соответствующих ячеек приведены в табл. 4.

Таблица 4

Стандартные типы и размеры данных

Тип

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

Размер

(Байт)

1

char

-128..127

1

2

unsigned char

0..255

1

3

int

-32 768.. 32 767

2

4

unsigned int

0..65535

2

5

long

-2 147 483 648..2 147 483 647

4

6

unsigned long

0..4 294 967 295

4

7

float

3.4e-38..3.4e+38

4

8

double

1.7e-308..1.7e+308

8

9

long double

3.4e-4932..3.4e+4932

10

Для того чтобы узнать размер ячейки соответствующего типа, достаточно написать в программе sizeof (тип). Дело в том, что для различных ОС размеры ячейки одного типа могут отличаться (например, тип int в 16- и 32-разрядных ОС, 1 байт и 2 байта соответственно).

Оператор sizeof вычисляет размер своего операнда в байтах. Операнд должен быть выражением, которое не вычисляется, или именем типа в скобках. Оператор sizeof нельзя применить к функции, битовому полю, неопределенному классу, типу void или к массиву с неуказанными границами индексов. Байт никак не определяется языком, кроме как результатом операции sizeof, а именно sizeof (char) есть 1.

2.1.1. Объявление переменных

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

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

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

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

Например:

char a;

int b,c;

double d;

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

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

В языке С++ существуют два квалификатора, управляющих доступом и модификацией: const и volatile.

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

Пример:

const char a=’\n’;

const int b=0,c=1;

const double d=2.121.

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

Особо следует отметить символьный тип (char). Формально символьный тип хранит числа в диапазоне от -128 до 127. Ячейка памяти, отводимая под char, занимает ровно 1 байт памяти, поэтому тип char можно использовать для представления одного байта памяти.

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

- кодовое: char ch=33; – в ячейку ch заносится символ “!” с кодом 33;

- символьное: char ch=’!’; – то же самое.

Оба представления можно считать одинаковыми, поскольку в памяти, в конечном итоге, хранятся числа, а не символы. Второй способ является предпочтительным, поскольку задается начертание символа, и при изучении программы это улучшает ее ясность. Все символы можно разделить на две группы: видимые и служебные. Видимые символы используются для представления текста (в этот набор входит также пробел, символ с кодом 32, обозначаемый как ’ ’). Коды видимых символов лежат в диапазоне от 32 до 255. Служебные символы используются в программе для вспомогательных действий, например, для форматирования текста.

В языке С/С++ предусмотрены управляющие символьные константы, которые приведены в табл. 5.

Таблица 5

Управляющие символьные константы

Оператор

Действие

\b

Пробел

\f

Прогон бумаги

\n

Новая строка

\r

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

\t

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

\

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

\

Одинарная кавычка

\0

Нуль

\\

Обратная косая черта

\v

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

\a

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

\?

Знак вопроса

\N

Восьмеричная константа N

\xN

Шестнадцатеричная константа N

Самые используемые служебные символы: перевод строки (код 13), возврат каретки (код 10), табуляция (код 9). Для задания их в программе в виде символьных констант используется сочетание двух видимых символов, а именно “\n”, “\r”, “\t” соответственно. Для представления символа “слэш“ используется конструкция “\\”.

В языке С существуют четыре спецификатора хранения: extern, static, register, auto.

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

спецификатор_хранения тип имя_переменной

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

Если локальная переменная объявлена с помощью спецификатора static, компилятор выделит для нее постоянное место для хранения, как и для глобальной переменной.

Применение спецификатора static к глобальной переменной заставляет компилятор создавать глобальную переменную, видимую только в пределах текущего файла. Несмотря на то, что эта переменная остается глобальной, в других файлах она не существует.

Спецификатор register был разработан для того, чтобы компилятор сохранял значение переменной в регистре центрального процессора, а не в памяти, как обычно. Это означает, что операции над регистровыми переменными выполняются намного быстрее.

2.2. Объявление указателя

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

Формат объявления указателя:

Тип*Имя;

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

unsigned int *a; //* переменная-указатель на тип unsigned

int */ double *x; //* переменная-указатель на тип double

Как и любая переменная, указатель, объявленный внутри тела функции, содержит случайное начальное значение. Если записать значение по этому адресу, то вероятно, произойдет аварийное завершение программы или зависание, поскольку С++ не контролирует принадлежность ячеек памяти. Чтобы этого избежать, инициализируйте указатели нулем. Пример: 

int *a=NULL;

char *b=NULL;

float *c=NULL;

2.2.1. Операции разыменования и взятия адреса

Это основные операции, используемые при работе с указателями. Операция взятия адреса (&) используется для получения адреса объекта и помещения его в указатель. Пример: 

int a=5; // объявили переменную типа int

int *pa=&a; // поместили в указатель «pa» адрес переменной «a»

Операция разыменования (*) осуществляет косвенный доступ к адресуемой величине через указатель.

int a=5; // объявили переменную типа int

int *pa=&a; // поместили в указатель pa адрес переменной a

int b=*pa; // в переменную b заносим содержимое a

int t, f=0, *a;

a = &t; //* переменной a присваивается адрес переменной t */

*a =f; //* переменной, находящейся по адресу, содержащемуся в переменной a, присваивается значение переменной f */

2.2.2. Указатели на указатели

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

Пример:

int a=5; // объявили переменную типа int

int *pa=&a; // поместили в указатель pa адрес переменной a

int **ppa=&pa; // в указатель на указатель ppa заносим адрес pa

Разыменование указателя на указатель дает адрес, по которому хранится исходная величина.

Пример:

int a=5; // объявили переменную типа int

int *pa=&a; // поместили в указатель pa адрес переменной a

int **ppa=&pa; // в указатель на указатель ppa заносим адрес pa

**ppa=10; // значение a теперь равно 10

2.2.3. Арифметические операции с указателями

Над указателями можно выполнять унарные операции: инкремент и декремент. При выполнении операций ++ и -- значение указателя увеличивается или уменьшается на длину типа, на который ссылается используемый указатель, как показано ниже:

int *ptr, a[10];

ptr=&a[5];

ptr++; //* равно адресу элемента a[6] */

ptr--; //* равно адресу элемента a[5] */

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

int *ptr1, *ptr2, a[10];

int i=2;

ptr1=a+(i+4); //* равно адресу элемента a[6] */

ptr2=ptr1-i; //* равно адресу элемента a[4] */

В операции вычитания могут участвовать два указателя на один и тот же тип. Результат такой операции имеет тип int и равен числу элементов исходного типа между уменьшаемым и вычитаемым, причем если первый адрес младше, то результат имеет отрицательное значение:

int *ptr1, *ptr2, a[10];

int i;

ptr1=a+4;

ptr2=a+9;

i=ptr1-ptr2; /* равно 5 */

i=ptr2-ptr1; /* равно -5 */

Значения двух указателей на одинаковые типы можно сравнивать в операциях ==, ! =, <, <=, >, >= при этом значения указателей рассматриваются просто как целые числа, а результат сравнения равен 0 (ложь) или 1 (истина). Пример:

int *ptr1, *ptr2, a[10];

ptr1=a+5;

ptr2=a+7;

if (prt1>ptr2) a[3]=4;

В данном примере значение ptr1 меньше значения ptr2 и поэтому оператор a[3]=4 не будет выполнен.

2.3. Массивы

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

При объявлении массива необходимо указать тип элемента, имя и количество элементов (ячеек).

тип имя_переменной[размер]

Например:

int arr1[10];             // массив из 10 ячеек типа int;

double arr2[50];     // массив из 50 ячеек типа double;

Для доступа к i-му элементу используют запись:

vals[i] где i может принимать значение от 0 до N-1, N-число элементов.

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

В языке С/С++ массивы могут иметь больше двух размерностей. Максимально допустимое количество размерностей задается компилятором. Общий вид объявления многомерного массива таков:

тип имя_переменной[размер1] [размер2] [размер3]… [размерN]

Например:

int vals[5][7]; // двумерный массив, размером 5х7;

char text[10][25][80]; // трехмерный массив 10 x 25 x 80.

Таким образом, для двумерного массива объявлением является прямоугольная матрица, а для трехмерного - куб с ячейками. Однако в памяти ЭВМ все массивы похожи на цепочки – элементы располагаются друг за другом. Например: 

int a[2][3]; /* представлено в виде матрицы;

a[0][0] a[0][1] a[0][2];

a[1][0] a[1][1] a[1][2] */.

Массивы, имеющие больше трех размерностей, используются редко, поскольку они занимают слишком большой объем памяти. Например, четырехмерный массив символов размерностью 10×6×9×4 занимает 2160 байт.

2.3.1. Инициализация массивов

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

тип_массива имя_массива [размер1]…[размерN] = {список_значений};

Список_значений представляет собой список констант, разделенных запятыми. Тип констант должен быть совместным с типом массива. Первая константа присваивается первому элементу массива, вторая – второму и т.д. Обратите внимание на то, что после закрывающейся фигурной скобки } обязательно должна стоять точка с запятой.

Инициализация одномерного массива осуществляется так:

int vals[10]={1,2,3,4,5,6,7,8,10};

Инициализация двумерного массива может быть выполнена следующим образом:

int b[2][2] = { { 1,2 }, { 3,4 } };

При инициализации массива можно опустить одну или несколько размерностей:

int b[]3[] = { { 1,2 }, { 3,4 } };

Если при инициализации указано меньше значений для строк, то оставшиеся элементы инициализируются 0, т.е. при описании int b[2][2] = { { 1,2 }, { 3 } }; элементы первой строки получат значения 1 и 2, а второй 3 и 0.

2.3.2 Динамические массивы

Если до начала работы программы неизвестно, сколько в массиве элементов, в программе следует использовать динамические массивы. Память под них выделяется с помощью операции new или функции malloc в динамической области памяти во время выполнения программы. Адрес начала массива хранится в переменной, называемой указателем. Например:

int n = 10;

int *a = new int[n];

double *b - (double *)malloc(n *sizeof (double));

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

Обращение к элементу динамического массива осуществляется так же, как и к элементу обычного – например а[3]. Можно обратиться к элементу массива и другим способом – *(а + 3). В этом случае мы явно задаем те же действия, что выполняются при обращении к элементу массива обычным образом. Рассмотрим их подробнее. В переменной-указателе а хранится адрес начала массива. Для получения адреса третьего элемента к этому адресу прибавляется смещение 3. Операция сложения с константой для указателей учитывает размер адресуемых элементов, то есть на самом деле индекс умножается на длину элемента массива: а + 3 *sizeof(int). Затем с помощью операции * (разадресации) выполняется выборка значения из указанной области памяти.

Если динамический массив в какой-то момент работы программы перестает быть нужным и мы собираемся впоследствии использовать эту память повторно, необходимо освободить ее с помощью операции delete[], например:

delete [] a; // Размерность массива при этом не указывается.

Сумма элементов правее последнего отрицательного. Написать программу, которая для вещественного массива из п элементов определяет сумму его элементов, расположенных правее последнего отрицательного элемента.

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

Вот как выглядит построенная по этому алгоритму программа:

finclude <iostream.h>

int main()

{

int n;

cout << " Введите количество элементов "; cin >> n;

int i, ineg;

float sum, *a = new float [n]     // 1

cout << " Введите элементы массива ";

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 << " Сумма " << sum;

return 0;

}

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

Номер последнего отрицательного элемента массива формируется в переменной i neg. При просмотре массива с помощью оператора 3 в эту переменную последовательно записываются номера всех отрицательных элементов массива, таким образом, после выхода из цикла в ней остается номер самого последнего.

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

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

finclude <iostream.h>

int main()

{

int n;

cout << "Введите количество элементов: "; cin >> n;

float *a = new float [n];

int i;

cout << "Введите элементы массива: ";

for (i = 0; i < n; i++) cin >> a[i];

bool flag_neg = false;

float sum = 0;

for (i = n – 1; i >= 0; i++)

if (a[i]<0) { flag_neg = true; break; }

sum+= a[i];

}

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

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

2.3.3. Методы доступа к элементам массивов

В языке С++ между указателями и массивами существует тесная связь. Например, когда объявляется массив в виде int array [25], то этим определяется не только выделение памяти для двадцати пяти элементов массива, но и для указателя с именем array, значение которого равно адресу первого по счету (нулевого) элемента массива, т.е. сам массив остается безымянным, а доступ к элементам массива осуществляется через указатель с именем array.

С точки зрения синтаксиса языка указатель array является константой, значение которой можно использовать в выражениях, но изменить это значение нельзя. Поскольку имя массива является указателем, допустимо, например, такое присваивание, как показано ниже:

int array[25];

int *ptr;

ptr=array;

Здесь указатель ptr устанавливается на адрес первого элемента масcива, причем присваивание ptr=array можно записать в эквивалентной форме ptr=&array[0].

Для доступа к элементам массива существует два  различных  способа.

Первый способ связан с использованием обычных индексных выражений в квадратных скобках, например, array[16]=3 или array[i+2]=7. При таком способе доступа записываются два выражения, причем второе выражение заключается в квадратные скобки. Одно из этих выражений должно быть указателем, а второе - выражением целого типа. Последовательность записи этих выражений может быть любой, но в квадратных скобках записывается выражение, следующее вторым. Поэтому записи array[16] и 16[array] будут эквивалентными.

Указатель, используемый в индексном выражении, не обязательно должен быть константой, указывающей на какой-либо массив, это может быть и переменная. В частности, после выполнения присваивания ptr=array доступ к шестнадцатому элементу массива можно получить с помощью указателя ptr в форме ptr[16] или 16[ptr].

Второй способ доступа к элементам массива связан с использованием адресных выражений и операции разыменования в форме *(array+16)=3 или *(array+i+2)=7. При таком способе доступа адресное выражение, равное адресу шестнадцатого элемента массива, тоже может быть записано разными способами *(array+16) или *(16+array). При реализации на компьютере первый способ приводится ко второму, т.е. индексное выражение преобразуется к адресному. Для приведенных примеров array[16] и 16[array] преобразуются в *(array+16). Для доступа к начальному элементу массива (т.е. к элементу с нулевым индексом) можно использовать просто значение указателя array или ptr. Любое из присваиваний присваивает начальному элементу массива значение 2. Пример:

*array = 2;

array[0] = 2;

*(array+0) = 2;

*ptr = 2;

ptr[0] = 2;

2.3.4. Массивы указателей

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

Примеры объявлений:

int *pa[10]; // массив из 10 указателей на int

char *str[5]; // массив из 5 указателей на тип char

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

Пример:

char *mes[] =;

{

cout<<"Деление на 0" , "Файл не открылся", "Не хватает памяти";

}

printf("%s", mes[errcod]); // в errcod код ошибки (от 0 до 2)

2.4. Строки

Строки представляют собой особый массив символов, заканчивающийся символом с кодом 0. Такой символ имеет представление ’\0’. Инициализацию массива символов можно выполнить путем использования строкового литерала. Например: 

char stroka[ ] = "привет"; // инициализируется массив символов из 7 элементов, последним элементом (седьмым) будет символ ’\0’, которым завершаются все строковые литералы.

Можно использовать и традиционную запись:

char stroka[ ] = {’п’,’р’,’и’,’в’,’е’,’т’,’\0’};

В том случае, если задается размер массива, а строковый литерал длиннее, чем размер массива, то лишние символы отбрасываются.

Следующее объявление инициализирует переменную stroka как массив, состоящий из семи элементов:

char stroka[5] = "привет";

В переменную stroka попадают первые пять элементов литерала, а символы ’Т’ и ’\0’отбрасываются. Если строка короче, чем размер массива, то оставшиеся элементы массива заполняются символами с кодом 0.

2.5. Операции

В языке С/С++ предусмотрено большое количество операций. Операции разделяются на четыре основные группы:

арифметические;

- сравнения; логические;

- побитовые.

2.5.1. Арифметические операции

+, -, *, / – выполняются точно так же, как и в большинстве других языков программирования. Арифметические операции представлены в табл. 6.

Таблица 6

Арифметические операции

Операция

Действие

-

Вычитание, а также унарный минус

+

Сложение

*

Умножение

/

Деление

%

Деление по модулю

--

Декрементация

++

Инкрементация

*=

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

/=

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

Операции инкрементации и декрементации ++ и --. Операция ++ добавляет 1 к своему операнду, а операция -- вычитает ее.

Форма записи а++ ++а сказывается в составных выражениях. Если ++ стоит после операнда в сложном выражении, то увеличение произойдет после вычисления выражения (постфиксная форма). Если ++ стоит перед операндом, то увеличение на единицу произойдет до вычисления выражения (префиксная форма).

2.5.2 Операции сравнения и логические операции

В термине операция сравнения слово «сравнение» относится к значениям операндов. В термине логическая операция слово «логическая» относится к способу, которым устанавливаются эти отношения. Поскольку операции сравнения и логические операции тесно связаны друг с другом, мы рассмотрим их вместе. Операции сравнения и логические операции представлены в табл. 7. В основе операций сравнения и логических операций лежат понятия «истина» и «ложь». В языке С истинным считается любое значение, не равное нулю. Ложное значение всегда равно 0, если результат ложен, и 1, если результат истинен.

В языке С++ истинные и ложные значения различаются точно так же, но, помимо этого, предусмотрен особый тип данных bool и булевые константы true и false.

Таблица 7

Операции сравнения и логические операции

Операции сравнения

Операция

Действие

>

Больше

>=

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

<

Меньше

<=

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

!=

Не равно

Логические операции

Операция

Действие

&&

И

||

Или

!

НЕ

&=

И и присваивание

|=

ИЛИ и присваивание

^=

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

Операции сравнения и логические операции имеют более низкий приоритет, чем арифметические операции. Таким образом, выражение    10 > 1+12 будет вычислено так, будто оно записано следующим образом: 10 > (1+12).

2.5.3. Побитовые операции

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

Таблица 8

Побитовые операции

Операция

Действие

&

И

|

ИЛИ

^

Исключающее ИЛИ

~

Дополнение до единицы (НЕ)

>>

Сдвиг вправо

<<

Сдвиг влево

Контрольные вопросы

1. Организация данных в С/С++.

2. Типы данных в С/С++.

3. Объявление переменных. Классификаторы. Спецификаторы.

4. Строки, массивы, инициализация массивов.

5. Методы доступа к элементам массивов.

6. Операции.

7. Объявление указателя.

8. Арифметические операции с указателями.

9. Массивы указателей.


Лекция 3.

базовые Операторы в С/С++.

3.1. Базовые операторы

Оператор – это часть программы, которую можно выполнить отдельно (иными словами, оператор определяет некое действие).

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

Имя_переменной = выражение.

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

Операнды – это переменные, константы и другие выражения. Разделителями являются символы [] () , ; : ... * = # .

Отличия оператора присваивания:

- множественное присваивание: а=b=c=d=0;

- комбинированность: int a*=5.

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

При выполнении оператора присваивания результат приводится к типу переменной слева от знака “=”. В этом случае может возникнуть приведение длинного типа к более короткому. Явное преобразование типов задается путем указания названия типа в скобках (type).

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

Операторы языка С и С++ разделяются на следующие категории:

условные операторы (оператор условия if и оператор выбора switch);

операторы цикла (for, while, do while);

операторы перехода (break, continue, return, goto);

другие операторы (оператор "выражение", пустой оператор).

Операторы в программе могут объединяться в составные операторы с помощью фигурных скобок. Любой оператор в программе может быть помечен меткой, состоящей из имени и следующего за ним двоеточия. Все операторы языка C/C++, кроме составных операторов, заканчиваются точкой с запятой ";".

3.1.1. Оператор выражение

Выражение – это последовательность операндов, операций и символов – разделителей.

Любое выражение, которое заканчивается точкой с запятой, является оператором. Выполнение оператора выражение заключается в вычислении выражения.

Примеры:

++ i;

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

a=cos(b*5);

Этот оператор представляет выражение, включающее в себя операции присваивания, умножения и вызова функции.

fun(x,y);

Этот оператор представляет выражение, состоящее из вызова функции.

3.1.2. Пустой оператор

Пустой оператор состоит только из точки с запятой ";". При выполнении этого оператора ничего не происходит.

3.1.3. Составной оператор

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

3.2. Условные операторы

3.2.1. Оператор if

Формат оператора следующий:

if (выражение) оператор 1;

else оператор 2;

Выполнение оператора if начинается с вычисления выражения. Далее выполнение осуществляется по следующей схеме:

- если выражение истинно (т.е. отлично от 0), то выполняется оператор 1;

- если выражение ложно (т.е. равно 0), то выполняется оператор 2;

- если выражение ложно и отсутствует оператор 2 (в квадратные скобки заключена необязательная конструкция), то выполняется следующий за if оператор.

Следующий фрагмент иллюстрирует использование оператора if:

# include <iostream.h>

void main()

{

float a, b, c;

cout<<"a=";

cin>>a;

cout<<"b=";

cin>>b;

cout<<"c=";

cin>>c;

if (a= =0)

 {

 x=-c/b;

 cout<<"otvet:= "<<x<<"\n";

 }

else

 if (b==0)

  if (c<0)

  {

  x1=sqrt(-c/a);

  x2=-sqrt(-c/a);

  cout<<"otvet x1:= "<<x1<<"\n";

  cout<<"otvet x2:= "<<x2<<"\n";

  }

  else

  {

  cout<<"net reshenia"<<"\n";

  }

}

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

if (i)

{

if(j) оператор 1;

 if(k) оператор2; /* данный if */

 else оператор3; /* связан с данным оператором else */

}

else оператор4; /* связан с оператором if (i) */

Последний раздел else связан не с оператором if(j), который находится в другом блоке, а с оператором if (i). Внутренний раздел else связан с оператором if(k), потому что этот оператор if является ближайшим.

3.2.2. Оператор switch

Оператор switch предназначен для организации выбора из множества различных вариантов. Формат оператора следующий:

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

{

[объявление];

:

[ case константное-выражение1]: [список-операторов1];

[ case константное-выражение2]: [список-операторов2];

:

[ default: [список операторов]];

}

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

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

- вычисляется выражение в круглых скобках;

- вычисленные значения последовательно сравниваются с константными выражениями, следующими за ключевыми словами case;

- если одно из константных выражений совпадает со значением выражения, то управление передается на оператор, помеченный соответствующим ключевым словом case;

- если ни одно из константных выражений не равно выражению, то управление передается на оператор, помеченный ключевым словом default, а в случае его отсутствия управление передается на следующий после switch оператор.

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

void main()

{

 char k;

 int key;

do

{

 cout <<"Make your select, please!"\n";

 cin >> k;

 switch (k)

  {

  case '1': array ();key=1;break;

  case '2': kvadrat ();key=1;break;

  default: key=0;

 }

}

while(key= =1);

}

Операторы switch могут быть вложены друг в друга, даже если константы разделов case внешнего и внутреннего операторов switch совпадают.

3.3. Операторы перехода

В языке С/С++ предусмотрены четыре оператора безусловного перехода: return, goto, break, continue. Операторы return и goto можно применять в любом месте программы, в то время как операторы break и continue связаны с операторами циклов. Кроме того, break можно применять внутри оператора switch.

3.3.1. Оператор break

Оператор break обеспечивает прекращение выполнения самого внутреннего из объединяющих его операторов switch, do, for, while. После выполнения оператора break управление передается оператору, следующему за прерванным. Фрагмент, иллюстрирующий использование оператора break, представлен в предыдущем примере.

3.3.2. Оператор continue

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

3.3.3. Оператор return

Оператор return завершает выполнение функции, в которой он задан, и возвращает управление в вызывающую функцию, в точку, непосредственно следующую за вызовом.

Функция main передает управление операционной системе.

Формат оператора:

return (выражение);

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

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

int sum (int a, int b)

{

 return (a+b);

}

Функция sum имеет два формальных параметра a и b типа int и возвращает значение типа int, о чем говорит описатель, стоящий перед именем функции. Возвращаемое оператором return значение равно сумме фактических параметров.

3.3.4. Оператор goto

Использование оператора безусловного перехода goto в практике программирования на языке C/C++ настоятельно не рекомендуется, так как он затрудняет понимание программ и возможность их модификаций. Формат этого оператора следующий:

goto

имя-метки;

...

имя-метки:  

оператор;

Оператор goto передает управление на оператор, помеченный меткой имя-метки.

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

3.4. Операторы цикла

3.4.1. Оператор for

Оператор for - это наиболее общий способ организации цикла. Он имеет следующий формат:

for ( выражение 1 ; выражение 2 ; выражение 3 )

{

тело;

}

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

Выражение 2 - это выражение, определяющее условие, при котором тело цикла будет выполняться.

Выражение 3 определяет изменение переменных, управляющих циклом после каждого выполнения тела цикла. Схема выполнения оператора for следующая:

- вычисляется выражение 1;

- вычисляется выражение 2;

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

#include<iostream.h>

void main()

{

int i, x[10];

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

{

  cout<<"x["<<i+1<<"]=";

 cin>>x[i];

}

}

Оператор цикла будет выполняться от 0 до 9 раз и с каждым шагом его значение увеличится на 1.

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

for (;;)

{ ...

 break;

}

Так как согласно синтаксису языка C/C++ оператор может быть пустым, тело оператора for также может быть пустым. Такая форма оператора может быть использована для организации поиска.Оператор может быть пустым.

3.4.2. Оператор while

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

while (выражение);

{

тело;

}

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

- вычисляется выражение;

- если выражение ложно, то выполнение оператора while заканчивается и выполняется следующий по порядку оператор. Если выражение истинно, то выполняется тело оператора while;

- процесс повторяется.

Следующий фрагмент иллюстрирует использование оператора цикла с предусловием:

# include <iostream.h>

void main()

{

char key;

while (key= =’y’)

{

cout<<"[y/n]:";

cin>>key;

 }

}

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

3.4.3. Оператор do..while

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

do

{

тело;

}

while (выражение);

Схема выполнения оператора do while:

- выполняется тело цикла (которое может быть составным оператором);

- вычисляется выражение;

- если выражение ложно, то выполнение оператора do while заканчивается и выполняется следующий по порядку оператор;

- если выражение истинно, то выполнение оператора продолжается с пункта 1.

Следующий фрагмент иллюстрирует использование оператора цикла с постусловием.

# include <iostream.h>

void main()

{

char key;

do

{

cout<<"[y/n]:";

cin>>key;

}

while (key= = ‘y’);

}

Чтобы прервать выполнение цикла до того, как условие станет ложным, можно использовать оператор break. Операторы while и do while могут быть вложенными.

Контрольные вопросы

1. Условные операторы. Представьте в виде алгоритма.

2. Операторы перехода. Представьте в виде алгоритма.

3. Операторы цикла. Представьте в виде алгоритма.

4. Другие операторы.


Лекция 4.

Стандартный ввод/вывод.

Работа с файлами.

4.1. Роль стандартного ввода/вывода

Стандартный ввод/вывод в программировании на С/С++ играет исключительно важную роль. Программы, не включающие средства ввода/вывода, не могут взаимодействовать с внешними устройствами ЭВМ, поэтому являются бесполезными. Стандартный ввод/вывод связывает программу с двумя важнейшими устройствами: терминалом (stdout) и клавиатурой (stdin), обеспечивая интерфейс начального уровня с пользователем.

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

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

4.1.1. Основные функции стандартного ввода/вывода

Средства стандартного ввода/вывода реализованы в С в виде функций, основная часть которых расположена в библиотеке stdio. В С++ добавлена поддержка потоковых классов cin, cout. В табл. 9 перечислены основные функции.

Таблица 9

Основные функции стандартного ввода/вывода

Функция

Назначение

getchar()

чтение символа с клавиатуры с отображением его на экране

putchar()

вывод символа на экран

getch()

чтение символа без отображения на экране

gets()

чтение строки с клавиатуры

puts()

вывод строки на экран

scanf()

форматированный ввод с клавиатуры (с преобразованием по формату)

printf()

форматированный вывод на экран (с преобразованием по формату)

4.2. Понятие файла

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

Более широкое понятие файла используется в UNIX-подобных ОС. Поскольку языки С и С++ являются «родными» языками таких систем, в них можно найти отголоски концепций потокового ввода/вывода, перенаправления и буферизации информации. В UNIX-системах под файлом понимается не только пассивный набор данных, но и специальные структуры, связанные с физическими устройствами.

Например: ввод информации с клавиатуры – означает чтение из специального файла связанного со стандартным устройством ввода/вывода. Поэтому от понятия "файл" переходят к понятию "поток", которое является более общим и универсальным. Говоря о направлении информации на устройство или в область для хранения на диске, будем понимать вывод ее в поток, связанный с тем или иным устройством, областью. Точно так же, ввод из файла на диске или с клавиатуры подразумевает чтение информации из потока. По умолчанию будем подразумевать под файлами обычные файлы данных или регулярные файлы.

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

4.2.1. Строение файлов

Файлы могут иметь сколь угодно сложную организацию с точки зрения программиста и пользователя, но для ОС и программы файл представляет собой простую последовательность байт, оканчивающуюся специальным значением – EOF (End Of File, или Конец Файла).

Таким образом, формирование структуры файла, или ее интерпретация, происходит внутри программы. В этом смысле файл очень напоминает обычный массив данных, расположенный в оперативной памяти. Для доступа к данным внутри файла используется специальный файловый указатель (не путать с указателем на структуру FILE!). Основное отличие массива от набора данных файла заключается в том, что файловый набор является энергонезависимым и сохраняется на устройстве после выключения питания компьютера, в то время как данные в оперативной памяти существуют не дольше, чем время текущего сеанса работы программы. Значение EOF стараются сделать системно-зависимым, поэтому функции посимвольного чтения типа getc(), getchar() возвращают величину не типа char, как естественно ожидать, а int.

4.2.2. Порядок работы с файлом

Независимо от задачи работа с файлом предполагает следующую последовательность действий:

- объявление специальной переменной;

- открытие файла в режиме чтения, записи или добавления;

- изменение положения внутреннего указателя файла;

- запись, чтение или запись и чтение данных;

- закрытие файла.

После открытия файла, как правило, его внутренний указатель установлен в позицию первого байта. После операций чтения или записи значение указателя автоматически увеличивается на величину читаемого или записываемого блока. Указатель можно "двигать" и без операций чтения/записи, устанавливая его значение на любую величину, не превышающую размер файла в байтах. После окончания операций записи/чтения файл необходимо закрыть. Поскольку в системе одновременно может быть открыто небольшое число файлов (это значение задается в файле config.sys строкой FILES=N) и часть открытых файлов принадлежит самой ОС, то становится понятным требование не держать файлы постоянно открытыми, а закрывать их после окончания работы.

4.2.3. Обзор библиотечных функций С для работы с файлами

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

Главное отличие потоковых функций от префиксных заключается в том, что они выполняют дополнительную буферизацию информации. Использование префиксных функций может ускорить операции записи/чтения при работе с блоком данных, особенно, если его размер кратен величине сектора диска (512 байт). При работе с одиночными символами и строками оптимально использовать потоковые функции.

Существует также два режима доступа к содержимому файла: текстовый и двоичный. При чтении происходит изъятие символа возврата каретки. Все потоковые функции расположены в библиотеке stdio, поэтому в программу необходимо помещать командой #include заголовочный файл stdio.h. 

4.3. Программные конструкции при работе с файлами

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

4.3.1. Открытие/закрытие файла

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

#include <stdio.h>

#include <iostream.h>

FILE *fp;

fp=fopen(имя_файла, режим);

if(!fp)

cout<<”файл не открылся”;

else

cout<<”файл открылся”;

fclose(fp);

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

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

4.3.2. Цикл посимвольного чтения содержимого файла

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

#include <stdio.h>

#include <iostream.h>

FILE *fp;

int ch;     // Открытие и проверка

while((ch=fgetc(fp))!=EOF)   // операции над ch

{   

}      // закрытие файла

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

4.3.3. Цикл построчного чтения содержимого файла

Во многих задачах требуется обработать содержимое файла построчно. В этом случае на помощь приходит функция fgets, которая за одно обращение читает из файла строку или n-1 символов, если строки слишком длинные, например: 

#include <stdio.h>

#include <iostream.h>

FILE *fp char buf[256];   // Открытие и проверка

while(fgets(buf,256,fp))  // операции над содержимым buf

{

}      // закрытие файла

Контрольные вопросы

1. Роль стандартного ввода/вывода.

2. Основные функции стандартного ввода/вывода.

3. Понятие и строение файлов.

4. Каков порядок работы с файлом?

6. Какие программные конструкции вы знаете?


Лекция 5.

Функция. Пользовательские типы

данных.

5.1. Понятие функции

Функция – это независимая совокупность объявлений и операторов, обычно предназначенная для решения определенной задачи. Программы на С++ состоят по крайней мере из одной функции main, но могут содержать и больше функций. Различают полное и неполное объявление функции. Полное объявление, или определение, состоит из заголовка, в который входят тип возвращаемого значения, имя, формальные параметры, и тела, заключаемого в фигурные скобки. После закрывающейся фигурной скобки точка с запятой не ставится. При неполном объявлении используется заголовок, после которого стоит точка с запятой. В программе может быть только одно полное объявление и сколько угодно неполных. Последние, как правило, помещаются перед вызовом функции в программе, если определение расположено ниже по тексту или в другом файле с исходным кодом. При использовании большого количества функций неполные объявления помещают в «заголовочные» файлы, которые включаются в начало текста программой директивой препроцессора include. Вызов функции передает управление из вызывающей функции к вызванной. Фактические аргументы, если они есть, передаются по значению в вызванную функцию. При выполнении оператора return в вызванной функции управление и, возможно, возвращаемое значение передаются в вызывающую функцию.

5.1.1. Определение функции

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

тип_возвращаемого_значения имя_функции(список_параметров)

{

тело функции;

}

- тип возвращаемого значения - определяет тип переменной, которую возвращает функция;

- список параметров - это список (возможно пустой) формальных параметров, которые используются функцией;

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

В качестве типа возвращаемого значения может использоваться ключевое слово void.

Функцию вызывают по ее имени с указанием в круглых скобках перечня передаваемых параметров (если их нет, то в скобках ничего не указывается), например:

void Function1 (int n, char с)

{

}

int Function2

{

}

int main ( )

{

int x;

char y;

Functionl (x, y);

x = Function2 ( );

}

5.1.2. Формальные параметры

Формальные параметры – это переменные, которые принимают значения, переданные функции в точке вызова. Формальные параметры объявляются в списке параметров заголовка и заключаются в круглые скобки. Список параметров определяет имена параметров и порядок, в котором они принимают значения при вызове. Разделяются параметры в списке запятой. Тип каждого формального параметра должен соответствовать типу фактического аргумента.

5.1.3. Тип возвращаемого значения

Если функция предназначена для возврата значения некоторого типа, то для этого в ее теле используют ключевое слово return, после которого указывают возвращаемое значение. Пример: 

int power3(int n)

{

return n*n*n;

{

void main ( )

{

int x;

x = power3 (2); // x = 8

}

Все операторы после слова return игнорируются, и происходит возврат в вызывающую функцию.

5.1.4. Тело функции

Тело функции – это просто составной оператор. Составной оператор содержит операторы, которые определяют действия функции, и может также содержать объявления переменных, используемых в этих операторах. Все переменные, объявленные в теле функции, имеют тип памяти auto, если они не объявлены иначе. Когда вызывается функция, то создается память для локальных переменных и производится их инициализация (если она задана). Управление передается первому оператору составного оператора, и начинается процесс выполнения, который продолжается до тех пор, пока не встретится оператор return или конец тела функции.

5.1.5. Фактические параметры

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

void swap (int *a, int *b);

{

int t;

t=*a;

*a=*b;

}

void main()

{

int x=5, y=6;

swap (&x,&y);

}

В примере функция swap объявлена как функция с двумя аргументами типа указателей на int. Формальные параметры a и b объявлены так же, как указатели на целые величины. При вызове функции адрес x запоминается в a, и адрес y запоминается в b. Теперь два имени или "синонима" существуют для одной и той же ячейки памяти. Ссылки *a и *b в функции swap действуют точно так же, как x и y в main. Присваивание внутри функции swap изменяет содержимое x и y.

5.1.6. Рекурсивные вызовы

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

void f1 (int n);

{

int x;

f1 (x);

}

5.1.7. Передача параметров

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

void fun (int a)

{

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

}

void main()

{

int x=20;

fun (x); // не изменяем значение x

}

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

void fun(int *a)

{

*a=10; // присваиваем переменной x новое значение

}

void main()

{

int x=20;

fun(&x); // изменяем значение x

}

При передаче в функцию массива всегда реально передается адрес первого элемента, так что массивы передаются всегда по ссылке, как показано в примере: 

void fun(int *a, int N)

{

for(int i=0; i<N; i++)

a[i]=i*i; // заполняем массив

}

void main()

{

int x[10];

fun(x,10);

}

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

Наиболее важные моменты работы с функциями:

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

- интерфейс грамотно написанной функции определяется ее заголовком;

- для вызова функции надо указать ее имя и набор аргументов;

- в определении, в объявлении и при вызове функции типы и порядок следования аргументов и параметров должны совпадать;

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

- входные данные функции надо передавать по значению или по константной ссылке, результаты ее работы – через возвращаемое значение, а при необходимости передать более одной величины – через параметры по ссылке или указателю;

- при написании функции нужно предусмотреть все возможные ошибки и обеспечить пользователя функции средствами их диагностики;

- печать диагностических сообщений внутри функции нежелательна;

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

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

- рекурсивная функция должна содержать хотя бы одну нерекурсивную ветвь. При использовании рекурсии следует учитывать возникающие при этом проблемы и накладные расходы;

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

5.1.8. Библиотеки стандартных функций

В данном разделе представлены не все стандартные функции. Полный список основных функций можно найти в любом справочнике по С++.

Функции ввода-вывода языка С++:

Функция clearerr

#include <cstdio.h>

void clearerr (file *stream);

Функция clearerr () сбрасывает флаг ошибки, связанный с потоком, на который ссылается указатель stream, а также обнуляет индикатор конца файла.

Функция fclose

#include <cstdio.h>

int fclose (file *stream);

Функция fclose () закрывает файл, связанный с указателем stream, и очищает буфер. После выполнения функции fclose () указатель stream больше не связан с файлом, и любой автоматически размещенный в памяти буфер удаляется из нее.

Функция feof

#include <cstdio.h>

int feof (file *stream);

Функция feof () проверяет позицию файлового курсора, чтобы распознать конец файла, связанного с указателем stream. Если курсор находится в конце файла, возвращается ненулевое значение, в противном случае возвращается нуль.

Функция ferror

#include <cstdio.h>

int ferror (file *stream);

Функция ferror() анализирует ошибку, связанную с файлом, на который ссылается указатель stream. Если никаких ошибок не обнаружено, возвращается нуль, в противном случае возвращается ненулевое значение.

Функция fflush

#include <cstdio.h>

int fflush (file *stream);

Если указатель stream связан с файлом, открытым для записи, вызов функции fflush () принудительно записывает содержание буфера в файл, который остается открытым.

Функция fgetc

#include <cstdio.h>

int fgetc (file *stream);

Функция fgetc () извлекает из входного потока stream следующий символ и увеличивает файловый курсор на единицу. Символ считывается, как значение типа unsigned char, и преобразуется в целое число.

Функция fgetpos

#include <cstdio.h>

int fgetpos (file *stream, fops_t *position);

Функция fgetpos () сохраняет в объекте, на который ссылается указатель position, текущее значение файлового курсора. Объект, на который ссылается указатель position, должен иметь тип fops_t. Сохраняемое значение оказывается полезным, лишь если в дальнейшем вызывается функция fsetpos ().

Функция fopen

#include <cstdio.h>

file *fopen (const char *fname, const char *mode);

Функция fopen () открывает файл, имя которого задается параметром fname, и возвращает указатель на поток, связанный с этим файлом.

Функция fwrite

#include <cstdio.h>

size_t fwrite (const void *buf, size_t size, size_t count, file *stream);

Функция fwrite () записывает в поток stream массив, состоящий из count

 

Функция fprintf

#include <cstdio.h>

int fprintf (file *stream, const char *format,…);

Функция fprintf () выводит в поток, связанный с указателем stream, значения своих аргументов в соответствии со строкой format. Функция возвращает количество реально выделенных символов. При возникновении ошибки возвращается отрицательное число.

Функция freopen

#include <cstdio.h>

file *freopen (const char *fname, const char *mode, file *stream);

Функция freopen () связывает существующий поток с другим файлом. Имя нового файла задается параметром fname, режим доступа – mode, а перенаправляемый поток – указателем stream. Параметр mode принимает те же значения, что и функции fopen ().

Функция fprint

#include <cstdio.h>

int fprint (const char *format,…);

Функция fprint () записывает в поток stdout свои аргументы в соответствии с форматной строкой format.

Функция remove

#include <cstdio.h>

int remove (const char *fname);

Функция remove () стирает файл, заданный параметром fname. Если файл успешно удален, функция возвращает нуль, в противном случае возвращает ненулевое значение. Зависимая функция: rename ().

Функция rename

#include <cstdio.h>

int rename (const char *oldname, const char *newname);

Функция rename () меняет имя файла, заданного указателем oldname, на имя, заданное параметром newname. Указатель newname не должен ссылаться ни на один из существующих файлов в текущем каталоге.

Математические функции, входящие в состав стандартной библиотеки, разделяются на следующие категории:

- тригонометрические;

- гиперболические;

- экспоненциальные и логарифмические;

- другие.

Для всех математических функций необходим заголовок <cmath>. (В программах на языке С используется заголовочный файл math.h).

Функция acos

#include <cmath>

float acos (float arg);

double acos (double arg);

long double acos (long double arg);

Функция acos () возвращает арккосинус аргумента arg. Значение аргумента функции acos () должно лежать в интервале от -1 до 1, иначе произойдет ошибка.

Функция asin

#include <cmath>

float asin (float arg);

double asin (double arg);

long double asin (long double arg);

Функция asin () возвращает арксинус аргумента arg. Значение аргумента функции asin () должно лежать в интервале от -1 до 1, иначе произойдет ошибка.

Функция atan

#include <cmath>

float atan (float arg);

double atan (double arg);

long double atan (long double arg);

Функция atan () возвращает арктангенс аргумента arg.

Функция cos

#include <cmath>

float cos (float arg);

double cos (double arg);

long double cos (long double arg);

Функция cos () возвращает косинус аргумента arg. Значение аргумента должно быть выражено в радианах.

Функция exp

#include <cmath>

float exp (float arg);

double exp (double arg);

long double exp (long double arg);

Функция exp () возвращает основание натурального логарифма е, возведенное в степень arg.

Функция fmod

#include <cmath>

float fmod (float x, float y);

double fmod (double x, double y);

long double fmod (long double x, long double y);

Функция fmod () возвращает остаток от деления x/y.

Функция log

#include <cmath>

float log (float num);

double log (double num);

long double log (long double num);

Функция log () возвращает натуральный логарифм числа num. 

Функция sin

#include <cmath>

float sin (float arg);

double sin (double arg);

long double sin (long double arg);

Функция sin () возвращает синус аргумента arg. Значение аргумента должно быть выражено в радианах.

Функция tan

#include <cmath>

float tan (float arg);

double tan (double arg);

long double tan (long double arg);

Функция tan () возвращает тангенс аргумента arg. Значение аргумента должно быть выражено в радианах.

Функция sqrt

#include <cmath>

float sqrt (float num);

double sqrt (double num);

long double sqrt (long double num);

Функция sqrt () возвращает квадратный корень num. Если значение аргумента num отрицательно, возникает ошибка, связанная с выходом из области определения функции. 

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

Функция asctime

#include <ctime>

char *asctime (const struct tm *ptr);

Функция asctime () возвращает указатель на строку, содержащую информацию, хранящуюся в структуре, на которую ссылается указатель ptr. Строка преобразуется следующим образом.

День месяц дата часы: минуты: секунды год \n \0.

Функция clock

#include <ctime>

clock_t clock (void);

Функция clock () возвращает приблизительное время работы вызывающей программы.

Функция ctime

#include <ctime>

char *ctime (const time_t *time);

Получив указатель на строку, содержащую календарное время, функция ctime () возвращает указатель на строку, имеющую следующий вид.

День месяц дата часы: минуты: секунды год \n \0.

Функция time

#include <ctime>

time_t time (time_t * time);

Функция time () возвращает текущее календарное время, установленное системой. Если системное время не установлено, функция возвращает число -1.

В стандартной библиотеке определен ряд служебных функций. К ним относятся функции, выполняющие преобразования, обработку списков аргументов, сортировку и поиск, а также генерирующие случайные числа. Многие из этих функций объявлены в заголовке <cstdlib>. (В программах на языке С ему соответствует заголовочный файл stdlib.h).

Функция abort

#include <cstdlib>

void abort (void);

Функция abort () приводит к аварийному завершению.

Функция rand

#include <cstdlib>

int rand (void);

Функция rand () генерирует последовательность псевдослучайных чисел. Каждый раз при вызове функции rand () возвращается целое число из диапазона от нуля до RAND_MAX.

Функция system

#include <cstdlib>

int system (const char *str);

Функция system () передает центральному процессору операционной системы команду, представленную в виде строки str.

Функция exit () не относится к управляющим операторам. Вызов стандартной библиотечной функции exit () приводит к прекращению работы программы и передаче управления операционной системе.

Функция exit () выглядит следующим образом.

void exit (int код_возврата);

Значение переменной код_возврата передается вызывающему процессу, в роли которого чаще всего выступает операционная система.

5.2. Пользовательские типы данных.

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

Вторая причина, из-за которой следует переходить к пользовательским типам данных, заключается в том, что стандартные типы не информативны, т.е. «int a» может означать целое число студентов, координату пиксела, номер открытого файла. Несмотря на то, что все эти понятия имеют характер целых чисел, их желательно не смешивать, используя один тип.

5.2.1. Ключевое слово typedef

Ключевое слово typedef используется для присвоения нового имени существующему типу. Допустим, утомительно писать в программе объявления с unsigned int, например: 

unsigned int x;

unsigned int y;

Присвоим unsigned int новое обозначение с помощью typedef 

typedef unsigned int UI;

UI x;

UI y;

Кроме задания типов с длинными описаниями более коротких псевдонимов, typedef используется для облегчения переносимости программ. Если машинно-зависимые типы объявить с помощью typedef, при переносе этих программ потребуется внести изменение только в эти операторы.

5.2.2. Перечислимый тип данных

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

enum COLORS { RED, GREEN, BLUE };

COLORS color;

switch (color)

{

case RED: ....;

case GREEN: ....;

case BLUE: ....;

}

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

5.2.3. Понятие структуры

Структура – это набор из одной или более переменных, возможно, различных типов, сгруппированных под одним именем для удобства обработки.

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

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

struct addr

{

список определений;

}

В приведенном ниже примере идентификатор student описывается, как структура:

struct STUDENT

{

char name[25];

int id, age;

}

struct DATE

{

int DAY;

int MONTH;

int YEAR;

}

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

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

Например:

strcpy(st1.name,"Иванов");

st2.id=st1.id;

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

struct PERSON

{

char NAME[NAMESIZE];

char ADDRESS[ADRSIZE];

long ZIPCODE; /* почтовый индекс */

long SS_NUMBER; /* код соц. Обеспечения */

double SALARY; /* зарплата */

DATE BIRTHDATE; /* дата рождения */

DATE HIREDATE; /* дата поступления на работу */

};

5.2.4. Указатели на структурный объект

Описание struct DATE *PD; говорит, что PD является указателем структуры типа DATE.

Запись PD->YEAR означает обращение к содержимому поля YEAR структурного объекта, адрес которого хранится в PD.

Так как PD указывает на структуру, то к члену YEAR можно обратиться и следующим образом (*PD).YEAR, но указатели структур используются настолько часто, что запись “->” оказывается удобным сокращением.

Круглые скобки в (*PD).YEAR необходимы, потому что операция указания члена старше, чем “*” . Обе операции, “->” и “.”, ассоциируются слева направо, так что конструкции слева и справа эквивалентны P->Q->MEMB (P->Q)->MEMB EMP.BIRTHDATE.MONTH (EMP.BIRTHDATE).MONTH.

Можно выделить основные моменты структурного типа:

- структуры применяются для логического объединения связанных между собой данных различных типов;

- после описания структурного типа ставится точка с запятой;

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

- для обращения к полю используется операция выбора: «точка» при обращении через имя структуры и “->” при обращении через указатель;

- структуры одного типа можно присваивать друг другу;

- ввод/вывод структур выполняется поэлементно;

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

Контрольные вопросы

1. Понятие и определение функции.

2. Формальные и фактические параметры функции.

3. Рекурсивные вызовы и передача параметров функции.

4. Пользовательские типы данных.

5. Указатели на структурный объект


Лекция 6.

Работа с динамической памятью.

Динамические структуры данных

6.1. Работа с динамической памятью

6.1.1. Статическое и динамическое распределение памяти

Традиционно принято выделять статическое и динамическое распределения памяти.

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

float b[100];

char buf[255];

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

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

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

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

6.1.2. Основные принципы динамического распределения

памяти

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

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

- указатель определен как локальный объект. В этом случае динамическая память будет недоступна при выходе за пределы блока и ее нужно освободить перед выходом из блока;

- указатель определен как локальный объект, с ключевым словом static. Динамическая память, выделенная однократно в блоке, доступна через указатель при каждом входе в блок. Память нужно освободить по окончании ее использования;

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

6.1.3. Способы работы с динамической памятью

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

Функции библиотеки alloc. Для выделения динамической памяти в библиотеке функции alloc существуют две функции, например: 

void *malloc(unsigned s);

void *calloc(unsigned n, unsigned m);

malloc возвращает указатель на блок динамической памяти длиной в s байт. При неудачном выделении возвращает NULL. Функция calloc возвращает указатель на блок динамической памяти для размещения n элементов по m байт каждый. При неудачном выделении возвращает NULL.

Примеры использования функций:

int *a=(int*)malloc(sizeof(int));  // динамический объект типа int

char *str=(char*)malloc(255*sizeof(char)); // массив из 255 char

При динамическом распределении памяти для массивов следует описать соответствующий указатель и присваивать ему значение. Одномерный массив a[10] из элементов типа float можно создать следующим образом с помощью функции calloc, как показано в следующем примере:

float *a;

a=(float*)(calloc(10,sizeof(float));

Для освобождения блока динамической памяти используется функция free, как показано в следующем примере: 

free(str);

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

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

free(a[i]);

free(a);

Операторы new и delete. В С++ появилась пара операторов, предназначенных для работы с динамической памятью – это new и delete. Их использование помогает упростить текст программы, делает его более прозрачным, например: 

int *a=new int; // объект типа int.

float *b=new float[N]; // массив из N объектов типа float.

...

delete a; // освободили память

delete []b;

Следует заметить, что функция alloc возвращают безликий указатель на void, который требуется в явном виде преобразовать в тип указателя слева от =.

При использовании оператора new это не требуется. Также не требуется вызывать sizeof для определения размера объекта.

6.2. Динамические структуры данных

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

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

struct Node

{

int d;

Node *p;

};

В зависимости от решаемой задачи в программах применяются различные виды динамических структур.

6.2.1. Стек

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

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

struct Node     // Элемент стека

{

int left. right;

Node* p;

};

Node* top = 01;    // Вершина стека

Удобно оформить занесение и выборку элемента в виде отдельных функций. Функция помещения в стек обычно называется push, а выборки – pop. Все необходимое передается функциям через параметры, не будем использовать отдельную функцию для занесения первого элемента в стек, так как если указателю top присвоить 0 перед первым обращением к функции push( ), то функция push вполне прилично справится с созданием первого элемента стека:

Node* push(Node* top, const int l, const int r) // Занесение в стек

{

Node* pv = new Node;     // 1

pv->left = l;       // 2

pv->right = r;       // 3

pv->p = top;       // 4

return pv;       // 5

}

Node* pop(Node* top, int& 1, int& r) // Выборка из стека

{

Node* pv = top->p;     // 6

1 = top->1eft;       // 7

r = top->right;       // 8

delete top;       // 9

return pv;       // 10

}

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

Прежде всего мы описываем вспомогательную переменную-указатель и заносим в нее адрес нового элемента стека, который создается с помощью операции new (оператор 1). Выделяется столько памяти, сколько необходимо для хранения структуры типа Node. В операторах 2 и 3 информационные поля этой структуры 1eft и right заполняются значениями переданных в функцию границ фрагмента массива. Доступ к этим полям выполняется через указатель pv и операцию выбора ->. Новый элемент становится вершиной стека. Поле его указателя должно ссылаться на элемент, помещенный в стек ранее. Эта ссылка создается в операторе 4. Если «заталкиваемый» в стек элемент является первым, то в качестве первого аргумента функции push () надо задать 0.

Функция push возвращает указатель на вершину стека. Им всегда является указатель на только что занесенный элемент (оператор 5).

Выборка из стека (функция pop) выполняется аналогично. Сначала из вершины стека выбирается указатель на его следующий элемент (оператор 6), который станет новой вершиной стека. Этот указатель является возвращаемым значением функции (оператор 10). Информационная часть элемента заносится в переменные l и r, которые передаются в вызывающую функцию по ссылке (операторы 7 и 8). После того как вся информация из элемента выбрана, его можно удалить (оператор 9).

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

6.2.2.Линейный список

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

const int l_name = 31;

struct Man     // Элемент списка

{

char name[l_name];

int birth_day;

float pay;

Man *next;

};

Man *beg;     // Указатель на начало списка

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

Контрольные вопросы

1. Статическое и динамическое распределение памяти.

2. Основные принципы динамического распределения памяти.

3. Способы работы с динамической памятью.

4. Динамические структуры данных.

5. Стек.

6. Линейный список.

Лекция 7.

объектно-ориентированное

программирование

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

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

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

Способ управления сложными системами был известен еще в древности – divide et impera (разделяй и властвуй). То есть выход – в декомпозиции системы на все меньшие и меньшие подсистемы, каждую из которых можно совершенствовать независимо. Но если в рамках структурного подхода декомпозиция понимается как разбиение алгоритма, тогда каждый из модулей системы выполняет один из этапов общего процесса, то ООП предлагает совершенно другой подход.

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

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

7.1. Критерии качества декомпозиции проекта

Со сложностью приложения трудно что-либо сделать – она определяется целью создания программы. А вот сложность реализации можно попытаться контролировать.

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

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

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

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

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

- сцепление (cohesion) внутри компонента – показатель, характеризующий степень взаимосвязи отдельных его частей.

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

- связанность (coupling) между компонентами – показатель, описывающий интерфейс между компонентом-клиентом и компонентом-сервером. Общее число входов и выходов сервера есть мера связанности. Чем меньше связанность между двумя компонентами, тем проще понять и отслеживать в будущем их взаимодействие. А так как в больших проектах эти компоненты часто разрабатываются разными людьми, то очень важно уменьшать связанность между компонентами.

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

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

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

7.2. Новые концепции программирования

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

- объектно-ориентированное программирование (ООП);

- унифицированный язык моделирования (UML);

- специализированные средства разработки программного обеспечения.

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

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

Типичная программа на языке С++ состоит из совокупности объектов, взаимодействующих между собой посредством вызова методов друг друга. Структура программы на С++ приводится на рис. 1.

Рис. 1. Объектно-ориентированный подход

Функции объекта, называемые в С++ методами или функциями-членами, обычно предназначены для доступа к данным объекта. Если необходимо считать какие-либо данные объекта, нужно вызвать соответствующий метод, который выполнит считывание и возвратит требуемое значение. Прямой доступ к данным не возможен. Данные сокрыты от внешнего воздействия, что защищает их от случайного изменения. Говорят, что данные и методы инкапсулированы. Термины сокрытие и инкапсуляция данных являются ключевыми в описании объектно-ориентированных языков.

Инкапсуляция – это ограничение доступа к данным и их объединение с методами, обрабатывающими эти данные. Доступ к отдельным частям класса регулируется с помощью специальных ключевых слов: public (открытая часть), private (закрытая часть) и protected (защищенная часть).

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

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

Класс – это тип, определяемый программистом, в котором объединяются структуры данных и функции их обработки.

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

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

Наследование – механизм получения нового класса из существующего. Производный класс создается путем дополнения или изменения существующего класса. Благодаря этому реализуется концепция повторного использования кода.

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

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

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

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

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

Специализированные средства разработки программного обеспечения являются наиболее широко употребительными. Наиболее популярной средой разработки  для С++ является продукт, совместно разработанный компаниями Microsoft и Borland.

7.3. Достоинства ООП

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

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

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

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

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

Деструктор – это метод класса, именем которого является имя класса, предваренное символом . Вызов деструктора производится при уничтожении объекта. Деструктор не имеет ни аргументов, ни возвращаемого значения.

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

7.4. Объекты и классы в ООП

7.4.1. Определение класса

Класс можно определить с помощью конструкции:

тип_класса имя_класса {компоненты класса}:

Точка с запятой в конце ставится обязательно. В этом определении:

- тип класса – одно из служебных слов class, struct;

- имя класса – идентификатор;

- компоненты класса – определения и объявления данных и принадлежащих классу функций.

Имя класса является по умолчанию именем типа объектов. Данные – это поля объекта, образующие его структуру. Значения полей определяют состояние объекта. Функция, являющаяся компонентом класса, называется методом класса. Методами класса определяются операции над объектами класса. Разрешается определять класс:

- с полями и методами;

- только с полями;

- только с методами;

- без полей и без методов.

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

Элементы класса типа struct по умолчанию открыты и доступны в других частях программы. Члены класса сlass по умолчанию закрыты и недоступны вне данного класса. Доступ к содержимому класса задается спецификаторами доступа, которые обозначаются ключевыми словами public и private. После ключевого слова требуется поставить знак «:» (двоеточие).

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

Первый пример содержит класс и два объекта этого класса. Несмотря па свою простоту, он демонстрирует синтаксис и основные черты классов C ++. Листинг программы SMALLOBJ.cpp приведен ниже.

#include <iostream> // демонстрирует простой небольшой объект

using namespace std;

class smallobj  // определение класса

{

private;

int somedata:  // поле класса

public;

void setdata(int d) // метод класса, изменяющий значение поля

{

somedata = d;

}

void showdata() // метод класса, отображающий значение поля

{

cout < “Значение поля равно” « somedata « endl;

}

};

int main()

{

smallobj s1, s2; // определение двух объектов класса smallobj

s1.setdata(1066); // вызовы метода setdata()

s2.setdata(1776);

s1.showdata(); // вызовы метода showdata()

s2 showdata();

return 0;

}

Класс smallobj, определенный в этой программе, содержит одно поле данных и два метода. Методы обеспечивают доступ к полю данных класса. Первый из методов присваивает полю значение, а второй метод выводит это значение на экран (возможно, незнакомые термины привели вас в недоумение, но скоро мы раскроем их смысл). Объединение данных и функций является стержневой идеей ООП. Это проиллюстрировано на рис. 2.

Объект является экземпляром класса, так же, как автомобиль является экземпляром колесного средства передвижения. Класс smallobj определяется в начале программы SMALLOBJ. Позже, в функции main(), мы определяем два объекта s1 и s2, являющихся экземплярами класса smallobj.

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

Значение поля равно 1076 - вывод объекта s1;

Значение ноля равно 1776 - вывод объекта s2.

Рассмотрим подробнее первую часть программы, где происходит определение класса smallobj. Затем мы обратимся к функции main(), в которой задействованы объекты класса smallobj.

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

Определение методов. Класс smallobj содержит два метода: setdata() и showdata(). Методы класса – это функции, входящие в состав класса. Тела обоих методов состоят из одного оператора, который записан на одной строке с фигурными скобками, ограничивающими тело функции. Разумеется, можно использовать и более традиционный способ оформления функций:

void setdata(int d)

{

somedata = d;

}

и

void showdata()

{

cout << “\nЗначение поля равно” <<somedata;

}

Поскольку методы setdata() и showdata() описаны с ключевым словом public, они доступны за пределами класса smallobj.

Определение объектов. Первым оператором функции main() является:

smallobj s1, s2;

Этот оператор определяет два объекта s1 и s2 класса smallobj. Обратите внимание на то, что при определении класса smallobj не создаются никакие его объекты. Определение класса лишь задает вид будущего объекта, подобно тому, как определение структуры не выделяет память под структурные переменные, а лишь описывает их организацию. Все операции программа производит с объектами. Определение объекта похоже на определение переменной: оно означает выделение памяти, необходимой для хранения объекта.

Следующая пара операторов осуществляет вызов метода setdata():

sl.setdata(1066);

sl.setdata(1776);

Эти операторы выглядят не так, как обычный вызов функции. Эти объекты s1 и s2 связаны с именами функций операцией точки (.). Такой синтаксис объясняется тем, что вызов применим к методу конкретного объекта. Поскольку setdata() является методом класса smallobj, его вызов должен быть связан с объектом этого класса. Для того чтобы получить доступ к методу класса, необходимо использовать операцию точки (.), связывающую метод с именем объекта. Синтаксически это напоминает доступ к полям структуры, но скобки позади имени метода говорят о том, что мы совершаем вызов функции, а не используем значение переменной (операцию точки называют операцией доступа к члену класса).

В некоторых объектно-ориентированных языках программирования вызовы методов объектов называют сообщениями. Так, например, вызов: s1.showdata(); можно рассматривать, как посылку сообщения объекту s1 с указанием вывести на экран свои данные.

7.4.2. Использование класса

Имея определение класса, можно объявлять переменные типа «имя_класса». Переменная класса называется объектом, или экземпляром класса. Класс объявляется один раз, а вот объектов можно создать столько, сколько необходимо. Объекты объявляются аналогично переменным встроенного типа:

имя_класса имя_объекта;   // скалярный объект

имя_класса *имя_объекта;   // указатель

имя_класса имя_объекта [количество]; // массив

При объявлении переменных можно задавать ключевые слова class и struct:

class имя_класса имя_объекта;  // скалярный объект

class имя_класса *имя_объекта;  // указатель

struct имя_класса имя_объекта [количество]: // массив

Разрешается совмещать определение класса и объявление переменной:

class имя_класса { члены_класса } имя_объекта; // объект

class имя_класса { члены_класса } *имя_объекта; // указатель

struct имя_класса { члены_класса }

имя_объекта [ количество];   // массив

Эти переменные получают тип «имя_класса». Тогда в дальнейшем можно объявлять переменные без записи определения класса и без служебных слов class или struct, как показано выше. Объявленные переменные подчиняются правилам видимости, как и переменные встроенных типов, и время их жизни зависит от места объявления.

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

NullType a. *pb;  // скалярная переменная и указатель

Empty d[10];   // массив

date *pd:   // указатель

class Money t:  // скалярная переменная

class Money *p;  // указатель

class Money m[100];  // массив

XXX YYY;   // скалярная переменная

Library L[10]. *pl :  // массив и указатель

class Class { /*... */ }  // определение класса

vl. v2[20];   // скалярная переменная и массив

Переменным vl и v2 присваивается тип Class, после чего для объявления других переменных достаточно указания типа Class, например

Class v3. v4[10]:

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

Money х = t;   // инициализация переменной

date   y(d);   // инициализация переменной

date &u = у;   // обязательная инициализация ссылки

Если поля открыты, разрешается обычная инициализация полей инициализатором структуры, например

class Person

{public: string Fio; double Summa;};

Person Kupaev - {"Купаев М.", 10000.00};

Указатели могут быть инициализированы так же, как и указатели на переменные встроенных типов:

Money *pt = new Money,*pp =pt,*ps; // объявление указателей

ps = new TMoney;   // и создание объектов

date *pd = new date(), *pr;  // объявление указателей

pr = new date();   // и создание объектов

Форма со скобками после типа выполняет обнуление полей динамического объекта.

Объекты класса разрешается определять в качестве полей другого класса:

class Count

{Money Summa;   // сумма на счете

unsigned long NumberCount; // номер счета

date D;    // дата открытия счета

public:

void Display();   // вывод на экран

};

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

Объекты класса можно передавать в качестве аргументов любыми способами (по значению, по ссылке, по указателю) и получать как результат из функции:

Money AddMoneyCconst Money &a, const Money &b); // по константной ссылке

Money Init(const long double &r); // возврат значения

void fKdate t);   // по значению

void f2(date &t);   //по ссылке

void f3(date *t);   // по указателю

Функция, возвращающая объект класса, может быть использована для инициализации переменной класса:

Money t = Init(200.56); Money r = AddMoney(x.y);

Можно объявлять объекты-константы класса с обязательной инициализацией:

const Money k – р;

const date d(t);

const Money t = Init(200.50);

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

объект.поле указатель->поле;

Вызов открытых методов осуществляется аналогично:

объект.метод(параметры);

указатель->метод (параметры);

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

#include <string>

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

std::string time::toString()

{std::string s = "";   // строка-результат

Std::string Digits = "0123456789":  // цифры для результата

unsigned char dl,d2;    // выделенная цифра

// формируем часы в строку

dl = hours/10; d2 = hours%10;   // получаем цифры часов

s=s+Digits[dl];    // прицепляем старшую цифру

s=s+Digits[d2];    // прицепляем младшую цифру

s+=".";    // завершение подстроки

// формируем минуты в строку

dl = minutes/10; d2 = minutesu%10;  // вычисляем цифры минут

s=s+Digits[dl]; s=s+Digits[d2];  

// формируем секунды в строку

dl = seconds/10; d2 = seconds%10; // получаем цифры секунд

s=s+Digits[dl]; =s+Digits[d2];

return s;

}

Метод возвращает текущее значение полей класса time в виде строки «чч.мм.сс».

7.4.3. Вложенные классы

Класс, объявленный внутри другого класса, называется вложенным. Он является членом объемлющего класса, и его определение может быть открыто (public) или закрыто (private). Уровень вложенности стандартом не ограничивается.

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

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

class External

{ //…

friend class Inner;  // Inner доступна приватная

// часть External

class Inner   // вложенный класс

{ friend class External;  // External доступна приватная

// часть Inner

//…

};

};

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

void External :: Inner :: MethodInner(const External &t)

{ //...

memlnner = t.MethodExterna();// вызов метода объемлющего класса

//…

};

Метод вложенного класса Methodlnner() получает ссылку на объект внешнего класса и обычным способом вызывает метод MethodExternal ().

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

External :: Inner *pointer;

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

class External

{ //...

friend class Inner; // Inner доступна приватная

// часть External

structure Inner { /* все элементы доступны в External */ };

//…

};

Внутри методов вложенного класса ключевое слово this является указателем на текущий объект вложенного класса.

Методы вложенного класса можно реализовать непосредственно внутри класса. Если же методы вложенного класса определяются вне класса, определение необходимо ставить вне самого внешнего из объемлющих классов – в области глобальной видимости. Имя метода в таком случае должно иметь префиксы; количество префиксов равно уровню вложенности классов.

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

class A

{ //...

class В;  // объявление вложенного класса

//...

//…

};

class A::B  // внешнее определение вложенного класса

{ //...

};

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

Контрольные вопросы

1. Какова роль стандартных библиотек?

2. Какие основные стандартные библиотеки С вы знаете?

3. Какие вы знаете концепции в программировании?

4. Объекты и классы ООП.

5. Достоинства и недостатки ООП.


Лекция 8.

Конструкторы и Перегрузка операций.

8.1. Перегрузка операций

В C++ разрешено перегружать встроенные операции – это одно из проявлений полиморфизма. Перегрузка операций не является обязательной в ООП – в языках Java и С# она отсутствует. Однако наличие перегрузки операций в C++ обеспечивает дополнительный уровень удобства при использовании новых типов данных.

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

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

1. Запрещена перегрузка следующих операций:

- sizeof() – определение размера аргумента;

- . (точка) – селектор компонента объекта;

- ?: – условная операция;

- :: – указание области видимости;

-  .* – выбор компонента класса через указатель;

-  # и ## – операции препроцессора.

2. Операции можно перегружать только для нового типа данных – нельзя перегрузить операцию для встроенного типа. В C++ новый тип данных можно образовать с помощью конструкций enum, union, struct и class.

3. Нельзя изменить приоритет перегружаемой операции и количество операндов. Унарная операция обязана иметь один операнд, бинарная – два операнда; не разрешается использовать параметры по умолчанию. Единственная операция, которая не имеет фиксированного количества операндов, – это операция вызова функции (). Операции «+», «-», «*», «&» допускается перегружать и как унарные, и как бинарные.

4. Операции можно перегружать либо как независимые внешние функции (только такой способ перегрузки допустим для enum), либо как методы класса. Четыре операции:

- присваивания =;

- вызова функции ();

- индексирования [];

- доступа по указателю ->;

- допускается перегружать только как методы класса. Эти операции в принципе нельзя перегрузить для конструкции enum.

5. Если операция перегружается как метод класса, то левым (или единственным) аргументом обязательно будет объект класса, для которого перегружается операция.

Прототип функции-операции выглядит следующим образом:

тип operator@(список параметров);

где @ – символ операции. Слово operator является зарезервированным словом и может использоваться только в определении или в функциональной форме вызова операции.

8.1.1. Перегрузка операций внешними функциями

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

тип operator@(параметр_1, параметр_2);

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

тип operator@(параметр_1, параметр_2);

тип operator@(параметр_2. параметр_1);

Хотя бы один из параметров должен быть нового типа. Параметры можно передавать любым допустимым способом. Обращение к операции выполняется двумя способами:

- инфиксная форма параметр_1 @ параметр 2;

- функциональная форма operator@ (параметр_1, параметр2).

Прототип унарной операции, перегружаемой независимой внешней функцией, отличается только количеством параметров:

тип operator@(параметр);

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

- инфиксная форма @параметр;

- функциональная форма operator@(параметр).

Операции инкремента ++ и декремента -- имеют две формы: префиксную и постфиксную. В определении постфиксной формы операции должен быть объявлен дополнительный параметр типа int.

тип operator@(параметр, int);

Этот параметр не должен использоваться в теле функции. Инфиксная форма обращения к такой операции – параметр@; при функциональной форме обращения необходимо задавать второй фиктивный аргумент, например: operator@(параметр,0);

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

enum Week { mon = 1, tue, wed, thu, fri, sat. sun = 0 };

Week operator+(const Week &m, const int &b)

{ Week t = Week(b + m);

return (t = Week(t%7));

}

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

Week operator+(const int &b, const Week &m)

{ return (m+b); }

Week operator++(Week &m)  // префиксная форма

{ return (m = Week(m+l)); }

Week operator++(Week &m, int) // постфиксная форма

{ Week t = m; m = Week(m+l); return t; }

void print(const Week &d) //вывод на экран названий дней недели

{ string Days[7] = {"Sunday", "Monday", "Tuesday".

        "Wednesday", "Thursday", "Friday", "Saturday"

        };

cout << Days[d] << endl;

};

Week m = sat:  // m = суббота

print(m+l);  // выводит 'Sunday'

print(2+m);  // выводит 'Monday'

print(operator+(m.D); // выводит 'Sunday'

print(operator+(2.m)); // выводит 'Monday'

m++;   // вызов постфиксной формы, m == sun

print(m);  // выводит 'Sunday'

print(++m);  // выводит 'Monday'

print(operator++(m)); // префиксная форма, выводит 'Tuesday'

print(operator++(m,0)); // постфиксная форма, выводит 'Tuesday'

print(m);  // выводит 'Wednesday'

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

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

struct Fraction

{ int num; int denum;  // поля - открыты

void reduce ();  // метод - открыт

};

Fraction operator+(const Fraction &l. const Fraction &r)

{ Fraction t;

// (a.b)+(c,d)=(ad+bc.bd)

t.denum = l.denum*r.denum; // знаменатель результата

t.num = 1.num*r.denum+r.num*l.denum;   // числитель результата

t.reduce():   // сокращение

return t:

}

Fraction a, b, c;

// ...

a = b + с;   // инфиксная форма

a - operator+(b.c);  // функциональная форма

8.1.2. Перегрузка операций методами класса

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

тип operator@();

где @ – символ операции. Возвращаемое значение может быть любого типа, в том числе и определяемого класса. Операция может возвращать значение, ссылку или указатель на объект. Разрешается указывать void на месте типа возвращаемого значения.

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

Префиксная форма инкремента (и декремента) должна иметь прототип:

тип& operator@();

Постфиксная операция инкремента (и декремента) должна иметь прототип:

тип operator@(int);

Тип – это «имя_класса», в котором определяется операция.

Прототип бинарной операции имеет один аргумент и выглядит таким образом:

тип operator@(параметр);

Параметры разрешается передавать любым удобным нам способом: по значению, по ссылке или по указателю. Возвращать метод-операция тоже может значение, указатель или ссылку на объект, в том числе и своего класса. Разрешается указывать void на месте типа возвращаемого значения.

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

тип класс::operator@()  // унарная операция

тип класс::operator@(параметр) // бинарная операция

Вызов унарной операции для объекта имеет форму;

@объект;

объект@;

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

объект.operator@()

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

объект.operator@(0)

Функциональная форма вызова для бинарной операции, определенной как метод класса, выглядит так:

объект.operator@(аргумент)

Операции присваивания являются бинарными операциями, поэтому должны иметь один аргумент. Если операция присваивания operator= не реализуется явным образом, она создается автоматически по умолчанию. Для любого класса автоматически создаваемая операция присваивания имеет прототип:

класс& operator=(const класс &r)

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

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

Money& Money::operator+=(const Money &b)

Любая внешняя функция, определенная как «друг» класса, имеет неограниченный доступ к любым элементам класса, в том числе и к закрытым. Для того чтобы внешняя функция стала другом класса, необходимо в интерфейсе класса задать ее прототип, добавив впереди слово friend, например:

friend Money operator*(const long double &a, const Money &b);

При реализации функции слово friend писать запрещается. Не указывается также и префикс класса.

В частности, операции ввода/вывода operator>> и operator<< практически всегда реализуются как внешние дружественные функции. Например, для класса Two с двумя полями х и у целого типа эти функции могут выглядеть так:

ostream& operator<<(ostream& t, const Two &r)

{ return (t << '(' << r.x << '.' << r.y << ')'); }

istream& operator>>(istreams t, Two &r)

{ t >> r.x >> r.y; return t; }

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

class F

{ int f; int m;

public:

F(int t, int r = 0)   // конструктор

{ f = t; m = г; }

F& operator++()   // префиксный инкремент

{ f++; return *this; }

F operator++(int)   // постфиксный инкремент

{ F t = *this; f++; return t; }

F& operator+=(const F& r) // сложение с присваиванием

{ f+=r.f; m+=r.m; return *this; }  

// дружественные функции

friend F operator+(const F &1, const F &r);

friend ostream& operator<<(ostream& t. const F &r)

friend istream& operator>>(istreams t. F Sr)

};

// реализация дружественных функций

F operator+(const F &l, const F &r)

{ F t = 1; t+=r; return t; }

// дружественные функции ввода/вывода

ostream& operator<<(ostream& t, const F &r)

{ return (t << r.f << '/' << r.m); }

istream& operator>>(istreams t. F &r)

{ t >> r.f >> r.m; return t; }    \

8.2. Конструкторы и деструктор

Конструктор – это особый метод, имеющий имя, совпадающее с именем класса. Назначением конструктора является создание и инициализация объектов.

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

Заголовок конструктора, реализуемого вне класса, как и для обычного метода, должен сопровождаться префиксом:

класс::класс(параметры)

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

- конструктор без аргументов (конструктор по умолчанию);

- конструктор инициализации;

- конструктор копирования (с одним параметром определяемого класса).

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

класс::класс(){}

Конструктор копирования автоматически создается в таком виде:

класс::класс (const класс &r)

{ *this = r; }

Аргументом конструктора копирования всегда является объект своего класса. Эти конструкторы обеспечивают объявление объектов без инициализации и с инициализацией другим объектом того же типа:

date d;     // конструктор без аргументов

date t(d);     // конструктор копирования

date r = d;     // конструктор копирования

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

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

Любой конструктор с параметрами не своего класса является конструктором инициализации. Конструктор инициализации предназначен для инициализации полей класса.

class time

{ public:

time();    // конструктор без аргументов

time (int h, int m = 0, int s = 0) // конструктор инициализации

{ hours = h; minutes = m; seconds = s; }

  private:

int hours, minutes, seconds: // закрытые поля

};

time::time()    // реализация вне класса

{ hours = minutes = seconds = 0; }

Конструктор инициализации обеспечивает объявление и инициализацию переменных:

time t(12,59,59), s(13,45), u = 14;

Конструктор инициализации может вызываться явно:

time R = time(10): // явный вызов конструктора

Конструктор инициализации со всеми аргументами по умолчанию играет роль конструктора без аргументов (конструктора по умолчанию).

Конструкторы вызываются и при создании динамических переменных:

time *a = new time;   // конструктор без аргументов

time *b = new time();   // конструктор без аргументов

time *d = new time(12,23);  // конструктор инициализации

time *f = new time(*d);  // конструктор копирования

Конструкторы обеспечивают привычную форму объявления констант:

const time d2(100,67);   // конструктор инициализации

const time d0 = 100;   // конструктор инициализации

const time d5 = time(100);  // явный вызов

Конструктор инициализации обеспечивает инициализацию массива:

time Т[3] - { 1,2,3 };

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

time T[0]=time(l);

time T[l]=time(2);

time T[2]=time(3);

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

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

класс::~класс(){}

8.2.1. Конструкторы и параметры

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

class Money

{ public: // конструктор инициализации без аргументов

Money(const double &r = 0.0) { Summa = d; }

friend bool operator==(const TMoney &a, const TMoney &b);

friend bool operator!=(const TMoney &a, const TMoney &b)

private:

double Summa:

};

// реализация дружественных функций

bool operator==(const TMoney &a, const TMoney &b)

{ return (a.Summa == b.Summa);  }

bool operator!=(const TMoney &a, const TMoney &b)

{ return !(a == b);  }

void fl(Money t);   // по значению

void f2(Money *t);   // по адресу

void f3(const Money &t);  // по константной ссылке

void f4(Money &t);   // по ссылке

Money d2(100.67);   // инициализация

fl(d2);    // по значению -

// вызов конструктора копирования

f2(&d2);    // по адресу -

// конструкторы не вызываются

f3(d2);  // по константной ссылке - не вызываются

f4(d2):  // по ссылке - не вызываются

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

void f1 (Money t = 100);

void f3(const Money &t = 200);

void f2(Money *t - new Money);

void f2(Money *t = new ТМопеу(300));

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

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

1. Инициализировать в списке инициализации можно и константные поля, и обычные поля-переменные.

2. Независимо от порядка перечисления полей в списке инициализации, поля получают значения в порядке объявления в классе.

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

4. Для инициализации поля встроенного типа нулем используется специальная форма инициализатора имя_поля() – выражение не задается; для поля-объекта некоторого класса вызывается конструктор без аргументов (по умолчанию).

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

6. Список инициализации выполняется до начала выполнения тела конструктора.

Контрольные вопросы

1. Перегрузка операций.

2. Перегрузка операций внешними функциями.

3. Перегрузка операций методами класса.

4.  Конструкторы и деструктор.

5. Конструкторы и параметры.

Лекция 9.

Наследование. Введение в Visual C++.

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

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

9.1. Простое открытое наследование

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

сlass имя_класса_потомка: [модификатор_доступа]   имя_базового_класса

{ тело_класса }

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

При разработке отдельных классов используются два модификатора доступа к элементам класса: public (общий) и private (личный). При наследовании применяется еще один: protected (защищенный). Защищенные элементы класса доступны только прямым наследникам и никому другому.

Возможны четыре варианта наследования: класс от класса; класс от структуры; структура от структуры; структура от класса.

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

Если модификатор наследования – public, наследование называется открытым. Модификатор protected определяет защищенное наследование, а слово private означает закрытое наследование.

9.1.1 Конструкторы и деструкторы при наследовании

Конструкторы не наследуются – они создаются в производном классе (если не определены программистом явно). Система поступает с конструкторами следующим образом:

- если в базовом классе нет конструкторов или есть конструктор без аргументов (или аргументы присваиваются по умолчанию), то в производном классе конструктор можно не писать – будут созданы конструктор копирования и конструктор без аргументов;

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

- при создании объекта производного класса сначала вызывается конструктор базового класса, затем – производного. Пример конструкторы в наследнике:

// Базовый класс - с конструкторами по умолчанию

class Money

{ public:  // конструктор инициализации и без аргументов

Money (const double &r = 0.0) { Surma = d; }

private:

double Summa;

};

// класс-наследник без конструкторов

class Backs: public Money { };

class Basis

{ int a,b;

public:  // только явные конструкторы с аргументами

Basis(int x, int у) { а=х; b=у; }

};

class Inherit: public Basis

{ int sum:

public:

Inherit(int x,  int y, int s)

:Basis(x,y)  // явный вызов конструктора

{ sum=s ; }

};

В классе Money определен конструктор инициализации, который играет роль и конструктора без аргументов, так как аргумент задан по умолчанию. В классе-наследнике Backs конструкторы можно не писать. Во втором случае в классе Basis определен только конструктор инициализации (конструктор копирования создается автоматически, а конструктор без аргументов – нет). В классе-наследнике Inherit конструктор базового класса явно вызывается в списке инициализации.

Деструктор класса, как и конструкторы, не наследуется, а создается. С деструкторами система поступает следующим образом:

- при отсутствии деструктора в производном классе система создает деструктор по умолчанию;

- деструктор базового класса вызывается в деструкторе производного класса автоматически независимо от того, определен он явно или создан системой;

- деструкторы вызываются (для уничтожения объектов) в порядке, обратном вызову конструкторов.

Создание и уничтожение объектов выполняется по принципу LIFO: последним создан – первым уничтожен.

9.1.2. Поля и методы при наследовании

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

class Point2

{ int x;  int у;

public:  // ...

};

class Point3: public Point2

{ int z;

public: // ...

};

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

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

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

class Base

{ int f; int m;

publiс:

Base():f(),m(){}   // конструктор без аргументов

Base (int t, int r = 0)  // конструктор инициализации

{ f = t; m = r;  }    

Base& operator++()  // префиксный инкремент

{ f++;  return *this;  }

Base operator++(int)   // постфиксный инкремент 

{ F t = *this; f++; return t; }

Base& operator+=(const F& r)  // сложение с присваиванием 

{ f+=r.f; m+=r.m; return *this; }

};

class Derive: public Base

{ public:

Derive& operator--() // новый метод: префиксный декремент

{ f--: return *this; }

};

9.1.3. Вложенные классы и наследование

Ограничений в наследовании вложенных классов нет: внешний класс может наследовать от вложенного и наоборот; вложенный класс может наследовать от вложенного.

class A {};   // внешний класс

class В {

public:

class С: public A {}: // вложенный класс наследует от внешнего

};

class D: public В:: С {}; // внешний класс наследует от вложенного

class E

{ class F: public B::C {}; // вложенный класс наследует от вложенного

};

Нужно следить только за видимостью базового класса в точке наследования. Например, от вложенного класса Е::F «снаружи» наследовать нельзя, так как класс находится в приватной части класса Е и вне его – невидим. Однако допустимо, чтобы еще один вложенный класс внутри Е наследовал от вложенного класса Е::F:

class E

{ class F: public В: :С {};

class G: public F {};

};

9.1.4. Закрытое наследование

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

При закрытом наследовании все элементы класса-наследника становятся приватными и недоступными программе-клиенту.

class Base

{ public:

void methodK);

void method2();

};

class Derive: private Base // наследуемые методы недоступны клиенту

{ };

Программа, использующая класс Derive, не может вызвать ни methodK), ни method2(). В наследнике нужно заново реализовать нужные методы, вызвав в методах наследника методы родителя.

class Derive: private Base                  // наследуемые методы недоступны клиенту

{   public:

void methodK) { Base: :methodl();  }

void method2() { Base::method2(): }: }:

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

Можно открыть методы базового класса с помощью using-объявления, которое имеет следующий синтаксис:

using <имя базового класса>::<имя в базовом классе>: В наследуемом классе это делается так:

class Derive: private Base                   // наследуемые методы недоступны клиенту

{    public:

using Base::methodl(); }:

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

9.1.5. Виртуальные функции

Связывание – это сопоставление вызова функции с телом. Для обычных методов связывание выполняется на этапе трансляции до запуска программы. Такое связывание называют «ранним», или статическим. При наследовании обычного метода его поведение не меняется в наследнике. Однако бывает необходимо, чтобы поведение некоторых методов базового класса и классов-наследников отличались. Чтобы добиться разного поведения в зависимости от типа, необходимо объявить функцию-метод виртуальной; в C++ это делается с помощью ключевого слова virtual. Виртуальные функции в совокупности с принципом подстановки обеспечивают механизм «позднего» (отложенного) или динамического связывания, которое работает во время выполнения программы. Далее представлен текст программы, использующий виртуальные методы.

class Base     // базовый класс

{ public:

virtual void print() const   // базовый метод

{ cout << "Base!" << endl;  }

class Derive: public Base  // производный класс

{ public:

virtual void print() const  // переопределенный метод

{ cout << "Derive!" << endl;  }

};

void F(Base &d)

{ d.print(); }  // предполагается вызов метода базового класса

Base W;     // объект базового класса

F(W);    // работает метод базового класса

Derive U;   // объект производного класса

F(U);    // работает метод производного класса

Base *c1 = &W;  // адрес объекта базового класса

cl->print();   // вызов базового метода

cl = &U;  // адрес объекта производного типа вместо базового

cl->print():   // вызывается производный метод

Класс, в котором определены виртуальные функции (хотя бы одна), называется полиморфным классом.

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

1. Виртуальная функция может быть только методом класса.

2. Любую перегружаемую операцию-метод класса можно сделать виртуальной, например операцию присваивания или операцию преобразования типа.

3. Виртуальная функция наследуется.

4. Виртуальная функция может быть константной.

5. Если в базовом классе определена виртуальная функция, то метод производного класса с таким же именем и прототипом (включая и тип возвращаемого значения, и константность метода) автоматически является виртуальным (слово virtual указывать необязательно) и замещает функцию-метод базового класса.

6. Статические методы не могут быть виртуальными.

7. Конструкторы не могут быть виртуальными.

8. Деструкторы могут (чаще – должны) быть виртуальными – это гарантирует корректное освобождение памяти через указатель базового класса.

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

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

class Base

{ public:   // перегрузка виртуальных методов

virtual int f() const { cout << "Base::f()"<< endl; return 0; }

virtual void f(const string &s) const

{ cout « "Base: :f(string)"« endl: }

};

class Derive: public Base

{ public:

virtual int f(int) const          // переопределение виртуальной функции

{ cout << "Derive::f(int)"<< endl; return 0: }

};

Base b. *pb;    // объекты базового типа

Derive d. *pd = &d;   // объекты производного типа

pb = &d;   // здесь нужна виртуальность

pb->f();   // вызывается базовый метод

pb->f("name");   // вызывается базовый метод

pb->f(l);    // ошибка!

pd->f(l);   // нормально - вызов метода наследника

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

((Derive *)pb)->f(l);

Родительские методы можно сделать доступными в классе-наследнике при помощи using-объявления.

class Derive: public Base

{ public:

virtual int f(int) const

{ cout << "Derived::f(int)"<< endl: return 0: }

using Base::f;    // разрешение использовать скрытые базовые методы

};

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

Виртуальную функцию можно вызвать невиртуально, если указать квалификатор класса:

Base *cl = new Derive();       // адресуется объект производного класса

cl->print();   // вызов метода-наследника

cl->Base::print();  // явно вызывается базовый метод

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

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

class One

{ virtual void f(void) {}

};

class Two

{ virtual void f(void) {}

virtual void g(void) {}

};

// ...

cout << sizeof(One) << endl;   // размер = 4

cout << sizeof(Two) << endl;   // размер = 4

9.1.6. Чистые виртуальные функции и абстрактные классы

Виртуальная функция, не имеющая определения тела, называется чистой (pure) и объявляется следующим образом:

virtual тип имя(параметры) = 0;

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

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

9.2. Введение в Visual C++

В связи с тем, что сегодня уровень сложности программного обеспечения очень высок, разработка приложений Windows значительно затрудняется. Программист должен затратить массу времени на решение стандартных задач по созданию многооконного интерфейса. Реализация технологии связывания и встраивания объектов – OLE – потребует от программиста еще более сложной работы.

Чтобы облегчить работу программиста, практически все современные компиляторы с языка C++ содержат специальные библиотеки классов. Такие библиотеки включают в себя практически весь программный интерфейс Windows и позволяют пользоваться при программировании средствами более высокого уровня, чем обычные вызовы функций. За счет этого значительно упрощается разработка приложений, имеющих сложный интерфейс пользователя, облегчается поддержка технологии OLE и взаимодействие с базами данных.

Подобные средства автоматизированного создания приложений включены в компилятор Microsoft Visual C++ и называются MFC AppWizard. Заполнив несколько диалоговых панелей, можно указать характеристики приложения и получить его тексты, снабженные обширными комментариями. MFC AppWizard позволяет создавать однооконные и многооконные приложения, а также приложения, не имеющие главного окна, – вместо него используется диалоговая панель. Можно также включить поддержку технологии OLE, баз данных, справочной системы.

MFC – это базовый набор (библиотека) классов, написанных на языке С++ и предназначенных для упрощения и ускорения процесса программирования под Windows.

9.3. Основы программирования под Windows

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

Все Windows-программы должны содержать специальную функцию, которая не используется в самой программе, но вызывается самой ОС. Эту функцию обычно называют функцией окна, или процедурой окна. Она вызывается Windows, когда системе необходимо передать сообщение в программу. Именно через нее осуществляется взаимодействие между программой и системой. Функция окна передает сообщение в своих аргументах. Согласно терминологии Windows, функции, вызываемые системой, называются функциями обратного вызова. Таким образом, функция окна является функцией обратного вызова.

В большинстве Windows-программ задача создания функции окна лежит на программисте. При использовании библиотеки MFC такая функция создается автоматически. В этом заключается одно из преимуществ библиотеки.

9.3.1. Типы данных в Windows

В программах под Windows в целом (и в использующих библиотеку MFC в частности) не слишком широко применяются стандартные типы данных из С или С++, такие как int или char*. Вместо них используются типы данных, определенные в различных библиотечных файлах. Наиболее часто используемыми типами являются HANDLE, HWND, BYTE, WORD, DWORD, UNIT, LONG, BOOL, LPSTR и LPCSTR. Тип HANDLE обозначает 32-разрядное целое, используемое в качестве дескриптора. Есть несколько похожих типов данных, но все они имеют ту же длину, что и HANDLE, и начинаются с литеры Н. Дескриптор – это просто число, определяющее некоторый ресурс. Например, тип HWND обозначает 32-разрядное целое дескриптора окна. В программах, использующих библиотеку MFC, дескрипторы применяются не столь широко, как это имеет место в традиционных программах. Тип BYTE обозначает 8-разрядное беззнаковое символьное значение, тип WORD 16-разрядное беззнаковое короткое целое, тип DWORD беззнаковое длинное целое, тип UNIT – беззнаковое 32-разрядное целое. Тип LONG эквивалентен типу long. Тип BOOL обозначает целое и используется, когда значение может быть либо истинным, либо ложным. Тип LPSTR определяет указатель на строку, а LPCSTR константный (const) указатель на строку.

9.4. Cреда Microsoft Developer Studio

9.4.1. Библиотека MFC

Главная часть библиотеки MFC состоит из классов, используемых для построения компонентов приложения. С каждым MFC-приложением связывается определяющий его на верхнем уровне объект theApp, принадлежащий классу, производному от CWinApp.

Как правило, структура приложения определяется архитектурой Document-View (документ-облик). Это означает, что приложение состоит из одного или нескольких документов – объектов, классы которых являются производными от класса CDocument (класс "документ"). С каждым из документов связаны один или несколько обликов - объектов классов, производных от CView (класс "облик ") и определяющих облик документа.

Класс CFrameWnd ("окна-рамки") и производные от него определяют окна-рамки на дисплее. Элементы управления, создаваемые при проектировании интерфейса пользователя, принадлежат семейству классов элементов управления. Появляющиеся в процессе работы приложения диалоговые окна – это объекты классов, производных от CDialog.

Классы CView, CFrameWnd, CDialog и все классы элементов управления наследуют свойства и поведение своего базового класса CWnd ("окно"), определяющего по существу Windows-окно. Этот класс в свою очередь является наследником базового класса CObject ("объект").

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

9.4.2. Архитектура приложения

У всех Windows-приложений существует фиксированная структура, определяемая функцией WinMain. Структура приложения, построенного из объектов классов библиотеки MFC, является еще более определенной.

Приложение состоит из объекта theApp, функции WinMain и некоторого количества других объектов. Сердцевина приложения – объект theApp – отвечает за создание всех остальных объектов и обработку очереди сообщений. Объект theApp является глобальным и создается еще до начала работы функции WinMain. Работа функции WinMain заключается в последовательном вызове двух методов объекта theApp: InitInstance и Run. В терминах сообщений можно сказать, WinMain посылает объекту theApp сообщение InitInstance, которое приводит в действие метод InitInstance.

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

Следующее сообщение, получаемое theApp, – Run – приводит в действие метод Run. Объект theApp циклически выбирает сообщения из очереди и инициирует обработку сообщений объектами приложения.

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

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

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

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

9.4.3. Каркас приложения

Наследование – одна из фундаментальных идей объектно-ориентированного программирования. Именно этот механизм наследования позволяет программисту дополнять и переопределять поведение базового класса, не вторгаясь в библиотеку MFC, которая остается неизменной. Все изменения делаются в собственном производном классе. Именно в этом и заключается работа программиста.

Объекты, из которых состоит приложение, являются объектами классов, производных от классов библиотеки MFC. Разработка приложения состоит в том, что программист берет из библиотеки MFC классы CWinApp, CFrameWnd, CDocument, CView и т.д. и строит производные классы. Приложение создается как совокупность объектов этих производных классов. Каждый объект несет в себе как наследуемые черты, определяемые базовыми классами, так и новые черты, добавленные программистом. Наследуемые черты определяют общую схему поведения, свойственную таким приложениям. Новые же черты позволяют реализовать специфические особенности поведения приложения, необходимые для решения стоящей перед ним задачи.

При определении производного класса программист может:

- переопределить некоторые методы базового класса, причем те методы, что не были переопределены, будут наследоваться в том виде, в каком они существуют в базовом классе;

- добавить новые методы;

- добавить новые переменные.

Приложение, построенное на основе библиотеки MFC, большая часть которого невидима, является основой всего приложения. Часть приложения, лежащая в библиотеке MFC, «framework», называется каркасом приложения. Рассмотрим работу приложения, как процесс взаимодействия между каркасом и частью приложения, разработанной программистом. Совершенно естественно, что в методах, определенных программистом, могут встречаться вызовы методов базового класса, что вполне можно рассматривать как вызов функции из библиотеки. Важнее, однако, что и метод производного класса, определенный программистом, может быть вызван из метода родительского класса. Другими словами, каркас и производный класс в этом смысле равноправны – их методы могут вызывать друг друга. Такое равноправие достигается благодаря виртуальным методам и полиморфизму, имеющимся в арсенале объектно-ориентированного программирования.

Если метод базового класса объявлен виртуальным и разработчик переопределил его в производном классе, это значит, что при вызове данного метода в некоторой полиморфной функции базового класса в момент исполнения будет вызван метод производного класса, и следовательно, каркас вызывает метод, определенный программистом. Точнее говоря, обращение к этому методу должно производиться через ссылку на производный объект либо через объект, являющийся формальным параметром и получающий при вызове в качестве своего значения объект производного класса. Когда вызывается виртуальный метод М1, переопределенный разработчиком то, согласно терминологии Visual C++, каркас посылает сообщение М1 объекту производного класса, а метод М1 этого объекта обрабатывает это сообщение. Если сообщение М1 послано объекту производного класса, а обработчик этого сообщения не задан программистом, объект наследует метод М1 ближайшего родительского класса, в котором определен этот метод. Если же обработчик такого сообщения создан программистом, он автоматически отменяет действия, предусмотренные родительским классом в отсутствие этого обработчика.

9.4.4. Проект приложения

О принципах устройства приложения рассказывалось выше. Теперь рассмотрим, как оно создается с помощью Visual C++. Сначала разберем одно важное понятие – проект. До сих пор приложение рассматривалось только как совокупность объектов базовых и производных классов. Но для обеспечения работы приложения требуется нечто большее – наряду с описанием классов необходимо описание ресурсов, связанных с приложением, нужна справочная система и т.п. Термин "проект" как раз и используется, когда имеется в виду такой, более общий взгляд на приложение.

В среде Visual C++ можно строить различные типы проектов. Такие проекты после их создания можно компилировать и запускать на исполнение. Фирма Microsoft разработала специальный инструментарий, облегчающий и ускоряющий создание проектов в среде Visual C++. Например, мастер MFC AppWizard (exe) позволяет создать проект Windows-приложения, который имеет однодокументный, многодокументный или диалоговый интерфейс и использует библиотеку MFC.

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

Контрольные вопросы

1. Простое открытое наследование.

2. Конструкторы и деструкторы при наследовании.

3. Поля и методы при наследовании.

4. Вложенные классы и наследование.

5. Закрытое наследование.

6. Виртуальные функции.

7. Введение в Visual C++.

8. Основы программирования под Windows.

9. Типы данных в Windows.


ЗАКЛЮЧЕНИЕ

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

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

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

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

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


Список Литературы

  1.  Страуструп, Б. Язык программирования C++: Специальное издание / Б. Страуструп. – М.: Бином, 2005. – 1104 с.
  2.  Франка, П. C++: Учебный курс / П. Франка. - Сб.П.: Питер, 2004. – 528 с.
  3.  Фридман, А.Л. Язык программирования Си++: Курс лекций / А.Л. Фридман – М.: Интернет-университет информационных технологий, 2003.
  4.  Глушаков, С.В. Программирование на Visual C++ / С.В. Глушаков. – М.: АСТ, 2003. – 726 с.
  5.  Шилдт, Г. Полный справочник по С++ / Г. Шилдт. - 4-е изд., пер. с англ. – М.: Издательский дом «Вильямс», 2006. – 800 с.
  6.  Лафоре, Р. Объектно-ориентированное программирование в С++, / Р. Лафоре. Классика Computer Science. - 4-е изд. – СПб.: Питер, 2006. – 928 с.
  7.  C/C++. Структурное программирование: Практикум / Т.А. Павловская, Ю.А. Щупак. – СПб.: Питер, 2003. – 240 с.
  8.  Павловская Т.А., Щупак Ю.А. C++. Объектно-ориентированное программирование: Практикум. – СПб.: Питер, 2006. – 265 с.
  9.  Лаптев В. В., Морозов А. В., Бокова А.В. C++. Объектно-ориентированное программирование. Задачи и упражнения. – СПб.: Питер, 2007. – 288 с.
  10.  Программирование на языке С для AVR и PIC микроконтроллеров./ Сост. Ю.А. Шпак – К.: “МК-Пресс”, 2006. – 400 с.


 

А также другие работы, которые могут Вас заинтересовать

36751. Изучение вращательного движения на маховике Обербека 107.5 KB
  Если на тело, закрепленное на неподвижной оси, действует сила, то тело приобретает угловое ускорение, направленное вдоль этой оси. Величина ускорения зависит не только от величины и направления силы, но и от точки ее приложения. Это отражено в понятии момента силы, который как и сила является векторной величиной. В случае вращения вокруг неподвижной оси угловое ускорение, направленное вдоль этой оси, определяется результирующей проекцией моментов всех сил на эту ось.
36753. Сведения о некоторых командах ОС UNIX 121.5 KB
  Команды поступающие от пользователей называют заданиями чтобы отличить их от системных процессов. Перевод процесса в фоновый режим Если вы запускаете какойто процесс путем запуска программы из командной строки то обычно процесс запускается как говорят на переднем плане . Это значит что процесс привязывается к терминалу с которого он запущен воспринимая ввод с этого терминала и осуществляя на него вывод.
36754. Форматирование таблиц 309 KB
  Вставка таблицы с помощью панели инструментов Рис. Окно Вставка таблицы Вы сами можете выбрать каким способом создавать таблицу: при помощи меню ТаблицаДобавить таблицу. указав в соответствующих полях ввода число строк и столбцов создаваемой таблицы или можно воспользоваться соответствующей кнопкой Добавить таблицу панели инструментов Нажав кнопку выделите не отпуская клавиши мыши нужное число ячеек в раскрывающемся поле рис. Первый способ создания таблицы удобно использовать если размеры таблицы превышают 5 столбцов...
36756. Определение главного фокусного расстояния тонких линз 212.5 KB
  Приборы и принадлежности: оптическая скамья с набором рейтеров осветитель с источником питания экран собирающая и рассеивающая линзы. Ее вершины и в этом случае можно считать совпадающими в точке называемой оптическим центром линзы. Причем ось проходящая через оптический центр линзы и центры кривизны ее преломляющих поверхностей называется главной оптической осью линзы. Если направить луч света параллельно главной оптической оси вблизи нее то преломившись он пройдет через точки или в зависимости от того слева или...
36757. Получение и исследование света с различными состояниями поляризации 230.5 KB
  Цель работы: изучить методы получения и анализа света с различными состояниями поляризации, сформулировать гипотезу исследования, установить связи между основными способами получения поляризованного излучения, выделить существующие различия между ними, определить этапы исследования.
36758. Определение постоянного Планка спектрометрическим методом 115.5 KB
  Цель работы: сформулировать гипотезу исследования по уровням сложности, проанализировать метод исследования спектра, исследовать спектр излучения атома водорода в видимой области спектра (серия Бальмера), определить постоянные Ридберга и Планка, объяснить методику их определения, выяснить, как соотносится сплошной и линейчатый спектры атома водорода.
36759. Система дистанционной поддержки в вузе (на примере центра дистанционной поддержки обучения РГПУ им. А. И. Герцена) 43.5 KB
  Сколько метакурсов предлагается в данном разделе Какие значки используются для обозначения метакурсов которые можно посетить: а без кодового слова б только по кодовому слову Откройте метакурс Демонстрация возможностей Moodle. Перечислите модули метакурса Демонстрация возможностей Moodle. Задание №3 Порядок выполнения: Выберите модуль Основные возможности метакурса Демонстрация возможностей Moodle.