МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РЕСПУБЛИКИ КАЗАХСТАН ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ имени ШАКАРИМА г. СЕМЕЙ Документ СМК 3 уровня УМКД УМКД 042-39. 1.ХХ/03- 2013 УМКД Учебно-методические материалы по дисциплине «Программирование» Редакция №____от_____ УЧЕБНО-МЕТОДИЧЕСКИИ КОМПЛЕКС ДИСЦИПЛИНЫ «Программирование» для специальности 5В011100 – «Информатика» УЧЕБНО-МЕТОДИЧЕСКИЕ МАТЕРИАЛЫ Семей 2013 СОДЕРЖАНИЕ 1 2 3 4 5 Глоссарий Лекции Практические и лабораторные занятия Курсовая работа (проект) Самостоятельная работа студента 1. ГЛОССАРИИ динамическая структура данных, состоящая из узлов, каждый из которых содержит, кроме данных, не более двух ссылок на различные бинарные деревья относительно независимый фрагмент программы, ограниченный Блок особыми символами (операторными скобками) интервал времени, в течение которого объект существует в памяти Время жизни объекта компьютера синтаксическая конструкция языка программирования, имеющая вид формульной записи последовательности операций над Выражение данными (операндами) переменные, областью определения которых является вся Глобальные программа или блок переменные специальный метод корректного освобождения памяти из-под Деструктор полиморфных объектов единица обмена данными между программой и внешней памятью Запись структурирование программы на модули особого вида, называемые классами, и объединение данных и процедур их Инкапсуляция обработки тип данных, позволяющий создать в программе новые объекты Класс данных и ввести связанные с ними операции и функции величина, не изменяющая свое значение в процессе работы Константа программы специальный метод, устанавливающий связь экземпляра объекта с Конструктор VMT минимальные единицы языка, имеющие самостоятельный смысл Лексемы Локальные переменные с ограниченной областью определения в программе переменные специальные функции для управления форматами потокового Манипулятор вывода подпрограммы, предназначенные для работы с полями объекта Метод совокупность связанных данных, состоящая из элементов одного Множества типа часть структурированной системы, выполняющая четко Модуль определенные функции понятие объектно-ориентированного программирования, которое состоит в том, что класс, определяемый на основе другого класса, Наследование наследует все или некоторые свойства и методы родительского класса правила определения участков программы, на которые Область видимости распространяется описание свойств и методов некоторого класса и где допустимо использование имен его свойств и методов структурированный тип данных, в своем описании отличающийся от структуры тем, что вместо ключевого слова struct используется Объединение слово union допустимое в языке программирования высокого уровня Оператор Бинарное дерево 3 Очередь Переменная Подпрограмма Полиморфные объекты Препроцессор Процедура Рекурсивная подпрограмма предложение, задающее целостное законченное действие ЭВМ или представляющее набор описаний динамическая структура данных, добавление элементов в которую выполняется в один конец, а выборка — из другого конца величина, которая во время работы программы может менять свое значение функционально самостоятельная часть программы, обладающая собственным именем и набором локальных имен объекты, фактический тип которых может изменяться во время выполнения программы программа, выполняющая предварительную обработку входных данных поименованная часть программы, которая может выполнять некоторые заданные действия над условными данными, определяемыми с помощью формальных параметров подпрограмма, в которой содержится обращение к самой себе структура данных, состоящая из некоторого количества однородных элементов данных, каждый из которых содержит Список указатель на следующий элемент упорядоченный набор элементов данных, в котором можно добавлять и удалять элементы, причем, новый элемент всегда Стек записывается в его конец, а очередной читаемый или удаляемый элемент также выбирается из его конца структурированный тип данных, представляющий собой Структура поименованную совокупность разнотипных элементов программа, с помощью которой можно подготовить и распечатать Текстовый редактор текстовые данные файл, представляющий собой последовательность строк символов Текстовый файл переменной длины множество допустимых в вычислительной системе значений, Тип данных объединенных совокупностью применимых к ним операций файл, хранящий данные в том же виде, в котором они Типизированный представлены в оперативной памяти файл переменные в программе, значениями которых являются адреса Указатели переменных, массивов Флаг двоичные коды, управляющие форматом выводимых значений форматирования именованная часть программы, результатом выполнения которой Функция является значение, присваиваемое имени функции Экземпляр объекта переменная объектного типа таблица виртуальных методов VMT 4 2. ЛЕКЦИИ Лекция 1. Тема. Основные понятия. язык Borland C++. Элементы языка Borland C++. Цель: Ознакомить студентов, с основными понятиями, которые встречаются при компиляции программ, а также рассмотреть понятия программных модулей, препроцессора, системы программирования. 1.1. Программные модули Исходный модуль(source code) – это текст программы на языке программирования. Объектный модуль (object code) – результат обработки компилятором исходного модуля. Объектный модуль не может быть выполнен. Это незавершенный вариант машинной программы. К объектному модулю в общем случае должны быть подсоединены модули стандартной библиотеки, и он должен быть настроен по месту выполнения. Исполняемый (абсолютный) модуль создает компоновщик (linker), объединяя в один общий модуль объектные модули, реализующие отдельные части алгоритма. На этом этапе к машинной программе подсоединяются необходимые функции стандартной библиотеки. Стандартная библиотека (library) – набор программных модулей, выполняющих наиболее часто встречающихся в программировании задачи: ввод, вывод данных, вычисление математических функций, сортировки, работа, с памятью и т.д. Модули библиотеки хранятся в откомпилированном виде. Ошибки, возникающие на этапе компиляции, - ошибки компиляции. Их разделяют на синтаксические и семантические. В период выполнения программы могут быть ошибки выполнения. Они появляются либо из-за некорректной постановки задачи, либо из-за недопустимых данных и др. Все программы, выполняемые на компьютере, составляют его программное обеспечение. Работают программы благодаря его аппаратному обеспечению. Распределение ресурсов компьютера при решении различных задач осуществляет операционная система – комплекс программных модулей. Она контролирует работу аппаратуры исполняемой программы и обеспечивает связь программиста с программой. Несколько объединенных между собой компьютеров образуют компьютерную сеть. Эти компьютеры могут поочередно использовать некоторые общие ресурсы, как аппаратные, так и программные. 1.2. Препроцессор До компиляции над программой обычно выполняются некоторые предварительные действия: подключение текстов других исходных модулей, формирование микроопределений, планирование условной компиляции и др. Эта работа выполняется так называемым препроцессором, обычно являющимся составной частью компилятора. Директивы препроцессора начинаются знаком # (на английском hash). Директива может занимать несколько строк. В конце каждой строки, имеющей продолжение (т.е. кроме последней), ставится обратная косая черта. Например, #define text Этот текст \ будет замещать \ слово text в программе. Чаще всего препроцессор используется для того, чтобы подсоединить к компилируемой программе файлы с текстами программных модулей пользователя и соответствующих разделов системной библиотеки. Например, #include<stdio.h> #include<iostream.h> #include<math.h> 5 #include<alloc.h> Первые две дерективы обеспечивают подсоединение к программе разделов библиотек, осуществляющих ввод/вывод данных, используемый в языке программирования Borland C++; третья строка – раздел библиотеки с математическими функциями; последняя – с функциями для работы с динамической памятью. #include“progr.cpp” Препроцессор данную директиву заменит текстом исходного модуля№ имеющего имя progr.cpp. Если имя задано в знаках < >, то поиск файла с этим именем выполняется в системных каталогах, т.е. в системной библиотеке языка Borland C++. Обычно в “ ” указывается имя файла программиста, текст которого необходимо включить в программу, в С++ это имя можно записывать и в знаки < >. Имя записывается либо с указанием полного пути к файлу, либо (как выше) поиск файла будет осуществляться только в текущем каталоге. 1.3. Система программирования Разработку, отладку и документирование программ осуществляют с помощью программных комплексов, называемых системами программирования. Основными компонентами системы программирования являются: Язык программирования; Интегрированная среда; Редактор связей (компоновщик, сборщик); Библиотеки различного назначения; Файлы документации. Интегрированная среда включает в себя: Редактор текстов; Подсистему справочной информации; Подсистему работы с файлами; Подсистему управления компиляцией и редактированием связей; Отладчик программ. Язык Borland C++ является языком среднего уровня. Он включает в себя элементы машинно-ориентированных языков, т.е. имеется возможность работать с битами, байтами, непосредственно обращаться к данным в оперативной памяти (векторам прерываний, видеобуферу, буферу клавиатуры и т.д.). В отличие от языка ассемблера он намного удобнее для написания прикладных и системных программ. 2.1. Алфавит языка Множество символов языка Borland C++ можно разделить на четыре группы. В первую группу входят буквы латинского алфавита и символ подчеркивания. Строчные буквы используются для написания ключевых слов языка. Одинаковые строчные и прописные буквы (например, а и А) имеют различные коды и при записи имен переменных (идентификаторов) в языке Borland C++ различаются. Буквы русского алфавита используются для вывода информации в текстах, комментариях. Вторую группу используемых символов составляют цифры: 0,1,....,9. В третью группу входят специальные символы. Большинство этих знаков используется для разных целей. Специальные символы: + (плюс), - (минус), * (звездочка ), / (дробная черта), = (равно), > (больше), < ( меньше), ; (точка с запятой ), & (амперсанд ), [ ] (квадратные скобки), { } (фигурные скобки), ( ) (круглые скобки), _ (знак подчеркивания), (пробел ), . (точка), , (запятая), : (двоеточие), # (номер), % (процент), ~ (поразрядное отрицание), ? (знак вопроса), ! ( восклицательный знак), \ (обратный слэш), / (дробная черта), | (вертикальная черта), ‘ (апостроф), “ (кавычки). 6 2.2. Идентификаторы Идентификатор – это имя, которым обозначается некоторый объект (данное) в программе. Данные в оперативной памяти размещаются по некоторым адресам, заранее неизвестным программисту. Для того чтобы в программе иметь возможность обращаться к данным и обрабатывать их, программист этим данным дает условные имена, которые компилятор в программе заменит адресами в оперативной памяти. Для записи идентификаторов используются буквы латинского алфавита, цифры и знаки подчеркивания. Идентификатор может начинаться с буквы или знака подчеркивания. Компилятор различает идентификаторы по первым тридцати двум символам. Примеры записи идентификаторов: sum, result, n, m, c10, Beta, beta, _function, letter, array и т.д. Ошибочные идентификаторы: a+b, -omega, 9c, &b, %f, long, int, if. Так как строчные и прописные буквы различаются то идентификаторы BETA, beta, Beta будут различными. При выборе идентификатора необходимо учитывать следующее: 1. Идентификатор не должен совпадать с ключевыми словами языка и именами функций из библиотеки языка Borland C++; 2. Не рекомендуется начинать идентификатор со знака подчеркивания, так как этот символ используется в именах некоторых библиотечных функций и при совпадении имен эти функции будут недоступны программе. 2.3. Ключевые слова Ключевые слова – это имена, используемые в языке Borland C++ с некоторым заранее определенным смыслом? Данные слова нельзя применять в качестве идентификаторов объектов (данных) пользователя. Ключевые слова сообщают компилятору о типе данных, способе их организации, о последовательности выполнения операторов. К ключевым словам относятся: auto, break, case, catch, char, class, const, continue, default, delete, do, double, else, enum, extern, float, friend, for, if, inline, int, long, new, operator, private, protected, public, register, return, short, signed, sizeof, struct, switch, template, throw, this, typedef, union, unsigned, void, volatile, while. Ключевые слова near, far, huge определяют тип (размер) указателя на данные, а слова _asm, cdecl, fortran, pascal используются для организации связи с функциями, написанными на других языках программирования. 6.4. Типы данных Следует различать тип данных и модификатор типа. Существуют следующие базовые типы: char (символьный), int (целый), float (вещественный), double (вещественный с двойной точностью), void (пустой тип). К модификаторам относятся: unsigned (беззнаковый),. Signed (знаковый), short (короткий),. Long (длинный). Тип данных и модификатор типа определяют: Формат хранения данных в оперативной памяти (внутреннее представление данных); Диапазон значений, в пределах которого может изменяться переменная; Операции, которые могут выполняться над данными соответствующего типа. Все типы данных можно разделить на две категории: скалярные и составные. К скалярным типам данных относятся - символы, арифметические (целые, вещественные), указатели, перечисления. К составным типам данных относятся – массив, структура, поля битов, объединение. 2.4.1. Переменные 7 Данные, значения которых во время выполнения программы можно изменять, называются переменными, неизменяемые данные называются константами. В программе все данные перед их использованием должны быть объявлены или определены. В операторах определения данных указываются тип данных и перечисляются через запятую имена переменных, для каждой переменной в соответствии с типом выделяется необходимое количество байтов памяти. Выделенному полю байтов присваивается имя переменной, которое в дальнейшем используется в программе. Идентификатор (имя переменной) может быть записан с квадратными скобками, круглыми скобками или перед ним может быть один или несколько знаков *(звездочка). Спецификатор типа – одно или несколько ключевых слов, определяющих тип переменной. Язык Borland C++ определяет стандартный набор основных типов данных (int, char, double), применяя которые пользователь может объявлять свои производные (структурированные) типы (массив, структура, и др.). Например: int j=10, m=3, n; float c=-1.3, l=-10.23, n; Определения и объявления переменных рекомендуется размещать в начале программного модуля. Приведем размеры и возможные диапазоны базовых типов данных (таблица 1): Таблица 1 Объем памяти, байт Диапазон значений Наименование типа Тип данных Символьный char 1 -128…127 Целый int -32768…32767 2 Короткий Short 2(1) -32768…32767(-128…127) Длинный Long 4 -2147483648…2147483647 Беззнаковый целый Unsigned int 0…65535 2 Беззнаковый длинный Unsigned long 4 0…424967295 Вещественный Float 4 3,14*10-38…3,14*1038 Вещественный с Double 8 1,7 *10-308 1,7 *10308 двойной точностью Сложные типы данных подразделяются на массивы, структуры (struct), объединения или смеси (union), перечисления (enum). 2.4.2. Константы Константой называется данное, неизменяемое в процессе выполнения программы. В языке Borland C++ используются следующие типы констант: целые, с плавающей точкой, символьные, и строковые литералы. Целая константа – это целое число, записанное в десятичной, шестнадцатеричной или восьмеричной системе счисления. Десятичная константа – любое целое десятичное число со знаком или без знака и начинающееся со значащей цифры. Восьмеричная константа – это целое число, записанное в восьмеричной системе счисления и начинающееся с обязательного нуля. Шестнадцатеричная константа начинается с обязательных знаков 0х или 0Х (нуль, х) и является записью числа в шестнадцатеричной системе. Примеры записи целых констант Десятичная Восьмеричная Шестнадцатеричная +15 +017 0хf -71 -087 -0x47 379 0573 0x17B Примеры записи символьных констант: ‘A’, ‘9’, ’+’, ‘%’, ‘-‘, ‘# ’. Примеры записи строковых констант: “JAVA”, “ЭВМ”, “int”. 8 ...{ const float f24=2.4, f13=1.3;... /*объявление констант вещественного типа*/ Лекция 2. Тема. Структура простой программы. Функции ввода-вывода. Форматы преобразования данных. Цель: Ознакомить с правильной записью структуры простой программы данного языка. Изучить правила составления текстов программ 3.1. Комментарии Комментарий – любая последовательность символов, начинающаяся парой символов /* и заканчивающаяся парой символов */ или начинающаяся // и до конца текущей строки. 3.2. Структура программы Программа, написанная на языке Borland C++, состоит из директив препроцессора, объявлений глобальных переменных, одной или нескольких функций, cреди которых одна главная (main) функция управляет работой всей программы. Общая структура программы на языке Borland C++ имеет вид: <директивы препроцессора> <определение типов пользователя – typedef> <прототипы функций> <определение глобальных объектов> <функции> Функции, в свою очередь, имеют структуру: <класс_памяти> <тип> <имя функции> (<объявление параметров>) { - начало функции <определение локальных объектов> <операции и операторы> } - конец функции Перед компиляцией программы на языке Borland C++ автоматически выполняется предварительная (препроцессорная) обработка текста программы. С помощью директив препроцессора задаются необходимые действия по преобразованию текста программы перед компиляцией. Директивы записываются по следующим правилам: все препроцессорные директивы должны начинаться с символа #; все директивы начинаются с первой позиции; сразу за символом # должно следовать наименование директивы, указывающее текущую операцию препроцессора. Наиболее распространены директивы #include и #define. Директива #include используется для подключения к программе заголовочных файлов (обычных текстов) с декларацией стандартных библиотечных функций. При заключении имени файла в угловые скобки < > поиск данного файла производится в стандартной директории с этими файлами. Если же имя файла заключено в двойные кавычки ” ”, то поиск данного файла осуществляется в текущем директории. Например: #include <stdio.h> - подключение файла с объявлением стандартных функций файлового ввода-вывода; #include <conio.h> - функции работы с консолью; #include <graphics.h> - графические функции; #include <math.h> - математические функции. Директива #define (определить) создает макроконстанту и ее действие распространяется на весь файл. Например: #define PI 3.1415927 9 В ходе препроцессорной обработки программы идентификатор PI заменяется значением 3,1415927. Пример программы: #include <stdio.h> #include < conio.h> /* Директивы препроцессора */ #define PI 3.1415927 void main() // Заголовок главной функции { // Начало функции int num; // Декларирование переменной num num=13; // Операция присваивания clrscr(); // Очистка экрана printf(«\n Число pi=%7.5f\n %d-это опасное число \n”, PI, num); } // Конец функции В первых двух строках программы указаны директивы препроцессора #include, по которым происходит подключение заголовочных файлов, содержащих декларации функций ввода-вывода (stdio.h) для функции printf() и работы с консолью (conio.h) для функции clrscr(). Следующая директива создает макроконстанту PI и подставляет вместо ее имени значение 3,1415927 по всему тексту программы. В главной функции main декларируется переменная целого типа num. Далее этой переменной присваивается значение 13. Функция printf выводит на экран строки: Число pi =3.1415927 13 – это опасное число Как видно, функция представляет собой набор операций и операторов, каждый из которых оканчивается символом ; (точка с запятой). В тексте программы использованы комментарии между парой символов /* */ и после пары символов //. 4.1. Функции вывода информации Для вывода информации в языке Borland C++ используются следующие функции: Функция putchar() обеспечивает вывод одиночного символа без перехода на новую строку. Функция puts() используется для вывода строки символов с переходом на начало новой строки. Функция printf() предназначена для форматированного вывода данных. Ее формат: рrintf (<управляющая строка>, <список аргументов>); Управляющая строка заключается в кавычки и указывает компилятору вид выводимой информации. Она может включать спецификации преобразования и управляющие символы. Спецификация преобразования имеет вид: % <флаг> <размер поля . точность> спецификация где флаг может принимать следующие значения: выравнивание влево выводимого числа (по умолчанию выполняется выравнивание вправо); + выводится знак положительного числа; размер поля – задает минимальную ширину поля, т.е. длину числа. При недостаточной ширине поля выполняется автоматическое расширение; точность – задает точность числа, т. Е. количество цифр в его дробной части; спецификация указывает вид выводимой информации. Ниже приведены основные форматы функции печати: Таблица 2 Формат Тип выводимой информации 10 Целое число Десятичное целое число Один символ Строка символов Число с плавающей точкой (экспоненциальная запись) Число с плавающей точкой (десятичная запись) Десятичное число без знака Восьмеричное число без знака Шестнадцатеричное число без знака Для длинных чисел (long, double) – используется дополнительный формат l. Например: %ld – длинное целое, %lf – вещественное число с удвоенной точностью. При необходимости вывода символа % его нужно указать 2 раза. Например: printf(«Только %d%% предприятий не работало.\n»,5); Получим: Только 5% предприятий не работало. %i %d %c %s %e %f %u %o %x Управляющая строка может содержать следующие управляющие символы: \n – переход на новую строку; \t – горизонтальная табуляция; \v – вертикальная табуляция; \b – сдвиг текущей позиции влево; \r – возврат в начало строки; \f – прогон бумаги до начала новой страницы; \a – звуковой сигнал; \ddd – 8-ричный ASCII-код; \xhhh – 16-ричный- -код; \? – знак вопроса. Список аргументов – печатаемые объекты (константы, переменные или выражения), вычисляемые перед выводом. Количество аргументов и их типы должны соответствовать спецификациям преобразования в управляющей строке. Пример: #include <stdio.h> #define PI 3.1415926 main() { int number=5, cost=11000, s=-777; float bat=255, x=12.345; printf(«%d студентов съели %f бутербродов.\n», number, bat); printf(«Значение числа pi равно%f.\n», PI); printf(«Любовь и голод правят миром.\n»); printf(«Стоимость этой вещи %d%s.\n», cost,»Руб.»); printf («x=%-8.4f s=%5d%8.2f», x, s, x); } В результате выполнения последней функции printf () на экране будет выведено: х=12.3450 s= -777 12.34 4.2. Функции ввода информации Функция getch () используется для ввода одиночных символов. Функия gets () обеспечивает ввод строки символов до нажатия клавиши ENTER. Функция scanf предназначена для форматированного ввода информации любого вида. Общий вид функции: scanf (<управляющая строка>, < список адресов>); 11 Для нее, как и для функции printf (), указывается управляющая строка. Однако функция scanf(), в отличие от функции printf (), использует в списке адресов указатели на переменные, т.е. их адреса. Для обозначения указателя перед именем переменной записывается символ &, обозначающий адрес переменной. Для ввода значений строковых переменных символ & не используется. При использовании формата %s строка вводится до первого пробела. Вводить данные можно как в одной строке через пробел, так и в разных строках. Данную особенность иллюстрирует следующий участок программы: ... int course; float grant; char name[20]; printf( «Укажите ваш курс, стипендию, имя \n»); scanf( «%d%f», &course, &grant); scanf( «%s», name); /* & отсутствует при указании массива символов */ ... Язык Borland C++ предусматривает альтернативную функциям printf() и scanf() возможность обработки ввода/вывода стандартных типов данных и строк. Простой пример: printf(“Введите число”); scanf(“%d”, &n); В Borland C++ записывается так: cout <<”введите число”; cin>>n; В первом операторе используются стандартный выходной поток cout и операция << (операция передачи в поток). Во втором операторе используются стандартный входной поток cin и операция >> (операция извлечения из потока). Эти операции не требуют форматирующих строк и спецификаторов преобразования для указания на тип входных и выходных данных, а также операции взятия адреса & переменной n. Для организации потокового ввода/вывода программы на Borland C++ должны содержать заголовочноый файл iostream.h. 4.3. Стандартные математические функции Декларации математических функций языка Borland C++ содержатся в файле <math.h>. В последующих записях аргументы x и y имеют тип double, параметр n имеет тип int. Аргументы тригонометрических функций задаются в радианах (2π радиан = 360). Все приведенные математические функции возвращают значение (результат) типа double. Таблица 3 Математическая функция Имя функции в языке Borland C++ sqrt(x) x |x| fabs(x) ex exp(x) y x pow(x,y) ln(x) log(x) lg10(x) log10(x) sin(x) sin(x) cos(x) cos(x) tg(x) tan(x) arcsin(x) asin(x) arccos(x) acos(x) arctg(x) atan(x) arctg(x/y) atan2(x,y) x -x sh(x)=1/2 (e -e ) sinh(x) ch(x)=1/2 (ex+e-x) cosh(x) 12 tgh(x) tanh(x) Остаток от деления x на y fmod(x,y) Наименьшее целое, которое >=x ceil(x) Наибольшее целое, которое <=x floor(x) 4.4. Операция присваивания Операция присваивания имеет две формы записи: 1. Полная форма: имя_переменной =выражение; Сначала вычисляется выражение, а затем результат присваивается имени_переменной. Например: y =(x+2)/(3*x)-5; С помощью одного оператора можнo присвоить одно значение нескольким переменным, например: x=y=z=0; /* x, y, z=0 */ или z=(x=y)*5; - сначала переменной x присваивается значение переменной y, далее вычисляется выражение x*5 , и результат присваивается переменной z. 2. Сокращенная форма: имя_переменной операция=выражениe; где операция – одна из арифметических операций (+ , -, *, /, %); Например: x*=5; /* x=x*5; */ s+=7; /* s=s+7; */ y/=x+3; /* y=y/(x+3); */ Сокращенная форма операции присваивания применяется тогда, когда переменная используется в обеих частях полной формы данного оператора. В языке Borland C++ существует операции уменьшения (--) и увеличения (++) значения переменной на 1. Операции могут быть префиксные (++i и –i) и постфиксные (i++ и i--). При использовании данной операции в выражении, в случае префиксной операции сначала выполняется сама операция (изменяется значение i), и только потом вычисляется выражение. В случае постфиксной операции – операция применяется после вычисления выражения. Например: b=7; n=1; 1. c=b*++n; /* n=n+1; c=b*n; т.е. c=14 */ 2. c=b*n++; /* c=b*n; n=n+1; т.е. c=7 */ 13 Лекция 3. Тема. Понятие пустого и составного операторов. Операторы перехода Цель: Ознакомиться с составным, пустым операторами, с операторами разветвлений (оператор выбора по условию if, оператор-переключатель switch). Научиться программировать линейные и разветвляющиеся алгоритмы. Составной оператор – это несколько операторов, собранных в блок с помощью фигурных скобок “{ }” или разделенных запятой. Такой блок можно рассматривать как один оператор. Пустой оператор – это просто знак “;”. Им можно заменить оператор там, где не нужно выполнять никакого действия. 5.1. Оператор разветвления (if) Операторы разветвления - выбирают в программе среди нескольких вариантов ее возможного продолжения единственный вариант вычислительного процесса. Оператор If имеет следующую общую форму записи: if (условие) оператор1; [else оператор2;] при выполнении оператора if сначала вычисляется условие. Если результат – истина (любое отличное от нуля значение), то выполняется оператор 1. Если результат анализа условия – ложь (равен 0), то выполняется оператор 2. Если слово else отсутствует, то оператор 1 пропускается, а управление передается на следующий после if оператор. В качестве условия может использоваться арифметическое, логическое выражение, выражение сравнения, целое число, переменная целого типа, вызов функции с соответствующим типом возвращаемого значения. Например: ... if(x=y) printf(“\n числа равны”); ... 5.2. Конструкция if – else В некоторых ситуациях необходимо указать не только оператор (блок операторов), выполняемый в случае истинности условия, но и оператор (или блок), выполняемый при ложности условия. If (условие) оператор1; else оператор2; Свойства такие же, как у предыдущей конструкции. Например: ... if(x>y) max=x; else max=y; ... 5.3. Вложенные else – if При помощи if и else можно также составлять else – if – конструкции, которые могут осуществлять проверку нескольких выражений. If (условие) operator1; else if (условие) operator2; else if (условие) operator3; else operator4; /* Пример: ввести число и определить, больше ли оно нуля, меньше или равно нулю */ #include<stdio.h> 14 #include<conio.h> void main() { int n; printf(“\n введите n”); scanf(“%i”,&n); if (n>0) printf(“\n число больше нуля”); if (n<0) printf(“\n число меньше нуля”); else printf(“\n число равно нулю”); } Свойства конструкции: Условия проверяются в том порядке, в котором они перечислены в программе; Если одно из условий истинно, то выполняется оператор, соответствующий этому условию, а проверка оставшихся условий не производится; Если ни одно из проверенных условий не дало истинного результата, то выполняются операторы, относящиеся к последнему else; Последний else является необязательным, следовательно, он и относящийся к нему оператор могут отсутствовать. Триадный оператор (оператор условного перехода ?). Его форма: имя_переменной =условие ? выражение_1 : выражение_2; Если условие истинно, то имени_переменной присваивается результат выражения_1, иначе – выражения_2. Например: найти наибольшее из двух чисел: max=a>b ? a : b; 5.4. Оператор выбора switch В программировании часто встречается задача выбора одного варианта решения задачи из многих существующих. Это можно сделать с помощью вложенных if...else. Однако более удобный способ – использование оператора выбора switch, общий формат которого следующий: switch(выражение) { case consnant1: оператор1; break; ... case consnantN: операторN; break; default: оператор; break; } где consnant1…consnantN – целые или символьные константы; default – выполняется, если результат выражения не совпал ни с одной константой; может отсутствовать; break – oператор завершения работы switch. После выполнения одной из ветвей case все остальные ветви будут опущены. Если оператор break не записан, то выполняются операторы следующей ветви case. Оператор switch проверяет, совпадает ли значение выражения с одним из значений, приведенных ниже констант. При совпадении выполняются операторы, стоящие после совпавшей константы. Например: ... switсh (i) { case 1: f=pow(x,2); break; case 2: f=fabs(x); break; case 3: f=sqrt(x); break; default: printf(“Ошибка!”);exit(1); 15 } f=f+5; ... Лекция 4. Тема. Операторы цикла Цель: Изучить циклические операторы for, while, do – while, научиться составлять и программировать циклические алгоритмы. В большинстве задач, встречающихся на практике, вычисления по некоторой группе формул необходимо выполнять многократно. Этот многократно повторяющийся участок вычислительного процесса называют циклом. Т.о., под циклом понимается оператор или группа операторов, повторяющихся некоторое количество раз. Каждый проход по телу цикла называется итерацией. 6.1. Оператор while Основная форма циклического оператора while: while (<условие>) <оператор>; где оператор – это простой, составной или пустой оператор. Цикл выполняется до тех пор, пока условие принимает значение «истина», т.е. выражение в скобках возвращает ненулевой результат. Это цикл с предусловием – сначала проверяется условие, затем выполняется оператор. Поэтому цикл while не выполнится ни разу, если изначально результат вычисления условия будет равен 0. 6.2. Оператор do...while Основная форма оператора do – while: do <оператор> while (<условие>); или do { <оператор_1>; <оператор_2>...<оператор_n>; } while (<условие>); где оператор – это простой, составной или пустой оператор. Оператор do...while – оператор цикла с постусловием, т.е. сначала выполняется оператор, а затем проверяется условие на истинность. Так как в цикле do...while условие проверяется в конце цикла, то цикл будет выполнен хотя бы один раз. В циклах типа while и do...while допустимы те же способы досрочного выхода из цикла и досрочное завершение текущего шага цикла, как и в операторе for, но в последнем случае в отличие от цикла for управление передается на проверку условия. Для предотвращения бесконечного цикла, внутри циклов while и do–while нужно предусмотреть изменение переменных, входящих в условие. 6.3. Оператор for При организации цикла, когда его тело должно быть выполнено фиксированное количество раз, осуществляются три операции: инициализация счетчика, сравнение его величины с некоторым граничным значением и изменение значения счетчика при каждом прохождении тела цикла. Основная форма оператора цикла for имеет вид: for (выражение_1; выражение_2; выражение_3 ) <оператор>; где выражение_1 – инициализация начального значения параметра цикла; выражение_2 – проверка условия на продолжение цикла; выражение_3 – изменение параметра цикла (коррекция параметра); оператор – простой или составной оператор языка Borland С++. 16 Схема работы оператора следующая: только один раз вначале вычисляется выражение_1, затем проверяется выражение_2, и если оно - «истина», то выполняется циклический участок программы, затем производится коррекция параметра, и так до тех пор, пока выражение_2 не примет значение «ложь». Например: for (k=1; k<5; k++) printf(“\n %d”, k); В результате выполнения этого оператора печатаются в столбик цифры от 1 до 4. В качестве параметра цикла можно использовать переменную любого базового типа. Например: for(ch=’a’; ch<=’z’; ch++) /* вывод на экран БУКВ */ printf(“ %c”,ch); /* латинского алфавита */ Необходимо тщательно контролировать структуру циклов for в программе, чтобы не получился бесконечный цикл (из которого нет выхода). Например: for(k=10; k>6;k++) printf(“бесконечный цикл\n”); Выйти из цикла досрочно можно следующими способами: 1. по дополнительному условию; 2. используя операторы: break; - завершения работы цикла, в котором находится break, управление передается на первый после цикла выполняемый оператор; exit(int Kod); - происходит выход из программы; return; - осуществляется выход из функции; с помощью оператора безусловного перехода goto <метка>; Досрочное завершение текущего циклического шага возможно при помощи дополнительного условия или оператора continue, который прерывает выполнение текущего шага цикла, т.е. пропускает операторы оставшейся части цикла и передает управление в головной оператор цикла для коррекции параметра и проверки условия. Передавать управление извне вовнутрь цикла запрещается. Любое из выражений цикла for в круглых скобках может отсутствовать, но символ «;» опускать нельзя. Например: int i=0; for(; i<3; i++) puts(“Hello!”); 6.4. Вложенные циклы В случае вложенных циклов один цикл находится внутри другого, например: for(i=nn;i<nk;i++) for(j=mn;j<mk;j++) оператор; где оператор – это простой, составной или пустой оператор. Внутренний цикл будет выполняться для каждого значения параметра i, удовлетворяющего условию внешнего цикла. Пример: int i,j; for(i=1;i<10;i++) /* ПЕЧАТЬ ТАБЛИЦЫ УМНОЖЕНИЯ */ { /* ЦЕЛЫХ ЧИСЕЛ */ for(j=1;j<4;j++) printf(“\n %d*%d=%2d”, i, j, i*j); printf(“\n”); } 17 Пример 1: N 1 . На печать программа должна выводить промежуточные и k 1 k окончательный результаты. Текст программы может иметь вид: #include <stdio.h> #include <conio.h> void main(void) { float s; int k,N; puts(“Введите N”); scanf(“%d”,&N); for (s=0,k=1; k<=N; k++) { s+=1.0/k; printf(« \n k=%d s=%f «, k, s); } printf(«\n ОТВЕТ: s=%f, Press any key...»,s); getch( ); } Вычислить S Лекция 5. Тема. Массивы Цель: Изучить правила работы с одномерными массивами, а также особенности работы со строковыми объектами, как одномерными символьными массивами. 7.1. Одномерные массивы Массив – конечномерная последовательность данных одного типа. Массив – объект сложного типа. Каждый элемент массива определяется именем массива и индексом (целое число), по которому к элементу массива производится доступ. Рассмотрим одномерные массивы. Индексы у массивов в языке Borland C++ начинаются с 0. В программе одномерный массив объявляется следующим образом: <Тип> <имя массива>[размер]; где, размер – количество элементов одномерного массива. Размер массива может задаваться константой или константным выражением. Нельзя задавать массив переменного размера, для этого существует отдельный механизм – динамическое выделение памяти. Пример объявления массива целого типа: int a[5]; В массиве а первый элемент а[0], второй – а[1], …, пятый - а[4]. В языке Borland С++ не проверяется выход индекса за пределы массива. Корректность использования индексов элементов массива должен контролировать программист. Пример работы с одномерным массивом В массиве x целого типа найти индекс и значение максимального элемента. Текст программы может быть следующим: #include <stdio.h> #include <conio.h> void main(void) { int x[10]; // индексы принимают значения от 0 до 9 18 int max, mmax, i ; puts("\n введите 10 чисел, по одному в строке \n"); for (i=0; i<10; i++) { scanf(“%i”,&x[i]);} max=x[0]; nmax=0; for (i=1; i<10; i++) if (x[i]>max) { max=x[i]; nmax=i; } printf(" \n max = %5i, nmax=%3i”,max,nmax); getch(); } 7.2. Многомерные массивы Кроме одномерных массивов возможна работа с многомерными массивами. Объявление многомерного массива: <тип><имя>[<размер 1>][<размер 2>]…[<размер N>]={{список начальных значений}, {список начальных значений},…}; Наиболее быстро изменяется последний индекс элементов массива, поскольку многомерные массивы размещаются в памяти компьютера в последовательности столбцов. Например, элементы двухмерного массива b[2][1] размещаются в памяти в следующем порядке: b[0][0], b[0][1], b[1][0], b[1][1], b[2][0], b[2][1]. Следующий пример иллюстрирует определение массива целого типа, состоящего из трех строк и четырех столбцов, с инициализацией начальных значений: int a[3][4] = {{0,1,2,0},{9,-2,0,0},{-7,1,6,8}}; Если в какой-то группе {…} отсутствует значение, то соответствующему элементу присваивается 0. Предыдущий оператор будет эквивалентен следующему определению: int a[3][4] = {{0,1,2},{9,-2},{-7,1,6,8}}; Пример программы Создать двумерный массив целых чисел NxM (N и M не более 50), используя функцию rand и вывести на экран в форме матрицы, N,M ввести с клавиатуры: #include<stdio.h> #include<stdlib.h> #include<conio.h> #define rnd (rand()/32768.0) // rand - генератор случайных чисел от 0 до int, rnd – от 0 до 1 void main(void) { int i, j, n, m, a[50][50]; puts(“\n Input n, m:”); scanf(“%d %d”,&n,&m); printf(“\n Array a \n”); for(i=0; i<n; i++) for(j=0; j<m; j++) { a[i][j]=rnd*10-5; // диапазон от –5 до 5 printf(“%d%c“, a[i][j], (j= =m-1)?’\n’:’ ‘); } getch(); } 7.3. Строки 19 В алгоритмическом языке PASCAL существует отдельный тип данных – строка, который объявляется с атрибутом string. В языке Borland C++ отдельного типа данных «строки символов» нет. Работа со строками реализована путем использования одномерных массивов типа char, т.е. строка символов – это одномерный массив типа char, заканчивающийся нулевым байтом. Нулевой байт – это байт, каждый бит которого равен нулю, при этом для нулевого байта определена символьная константа ´\0´ (признак окончания строки или нуль-терминатор). Поэтому, если строка должна содержать k символов, то в описании массива необходимо указать k+1 элемент. Например, описание: char a[7], означает, что строка содержит шесть символов, а последний байт отведен под нулевой. Строковая константа в языке С – это набор символов, заключенных в двойные кавычки. Например: “Лабораторная работа по строкам”. В конце строковой константы явно указывать символ ´ \0 ´ не нужно, так как это сделает компилятор языка С. Строки можно инициализировать при декларировании, например: char S1[10]=”123456789”, S2[]=”12345”; в последнем случае размер строки будет установлен по количеству символов. Для ввода строки с клавиатуры дисплея используются две стандартные библиотечные функции, прототипы которых приведены в файле stdio.h. Функция scanf( ) вводит значения для строковых переменных спецификатором ввода %S. Но надо помнить, что функция scanf( ) вводит символы до появления первого символа “пробел”. Библиотечная функция gets( ), обеспечивает ввод строки с пробелами внутри этой строки. При этом ввод строки символов завершается нажатием клавиши ENTER. Обе функции автоматически ставят в конец строки нулевой байт. И, кроме того, так как строка – это символьный массив, а имя массива – это указатель на его начало в памяти, то символ «&» перед именами строковых объектов при использовании этих функций указывать не надо. Вывод строк производится функциями printf( ) или puts( ). Обе функции выводят символьный массив до первого нулевого байта. Функция printf( ) не переводит курсор после вывода на начало новой строки, программист должен предусмотреть такой перевод в строке формата. Функция puts( ) автоматически переводит курсор после вывода строковой информации в начало новой строки. Остальные операции над строками выполняются с использованием стандартных функций. Декларации функций для работы со строками размещены в файле string.h. Вот некоторые из наиболее часто используемых: 1. Функция strcpy(S1, S2) - копирует содержимое строки S2 в строку S1. 2. Функция strcat(S1, S2) - присоединяет строку S2 к строке S1 и помещает ее в массив, где находилась строка S1, при этом строка S2 не изменяется. Нулевой байт, который завершал строку S1, заменяется первым символом строки S2. 3.Функция strcmp(S1, S2) сравнивает строки S1 и S2 и возвращает значение =0, если строки равны, т.е. содержит одно и то же число одинаковых символов; значение <0, если S1<S2;значение >0, если S1>S2. 4. Функция strlen(S) возвращает длину строки, при этом завершающий нулевой байт не учитывается. Пример работы со строковыми данными В программе значение строки вводится с клавиатуры, затем введенная строка распечатывается в обратном порядке. #include <stdio.h> #include <string.h> #include <conio.h> void main(void) { 20 char s[100]; // объявление символьного массива int i, k; puts(" Введите исходную строку"); gets(s); k=strlen(s); puts(" РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ \n"); for (i=k; i>=0; i--) printf("%c",s[i]); /* вывод элементов массива в обратном порядке */ printf("\n Press any key..."); getch(); } Лекция 6. Тема. Указатели Цель: Ознакомиться с основными понятиями, такими как указатели, рассмотреть операции над указателями, указатели на указатели, массивы указателей 8.1. Указатели и операции над адресами Обращение к объектам любого типа как операндам операций в языке Borland C++ может проводиться: - по имени, как мы до сих пор делали; - по указателю (косвенная адресация). Указатель – это переменная, которая может содержать адрес некоторого объекта в памяти компьютера, например адрес другой переменной. И через указатель, установленный на переменную можно обращаться к участку оперативной памяти, отведенной компилятором под ее значения. Указатель объявляется следующим образом: тип *идентификатор; Например: int *a, *d; float *f; Здесь объявлены указатели a, d, которые можно инициализировать адресами целочисленных переменных и указатель f, который можно инициализировать адресами вещественных переменных. С указателями связаны две унарные операции: & и *. Операция & означает «взять адрес» операнда (т.е. установить указатель на операнд). Данная операция допустима только над переменными. Операция * имеет смысл: «значение, расположенное по указанному адресу» и работает следующим образом: - Определяется местоположение в оперативной памяти переменной типа указатель. - Извлекается информация из этого участка памяти и трактуется как адрес переменной с типом, указанным в объявлении указателя. - Производится обращение к участку памяти по выделенному адресу для проведения некоторых действий. Пример 1: int x, /* переменная типа int */ *y; /* указатель на элемент данных типа int */ y=&x; /* y - адрес переменной x */ *y=1; /* косвенная адресация указателем поля x /* “по указанному адресу записать 1”, т.е. x=1; */ Пример 2: int i, j=8, k=5, *y; y=&i; *y=2; /* i=2 */ y=&j; /* переустановили указатель на переменную j */ *y+=i; /* j+=i , т.е. j=j+1 -> j=j+2=10 */ 21 y=&k; /*переустановили указатель на переменную k */ k+=*y; /* k+=k, k=k+k = 10 */ (*y)++; /* k++, k=k+1 = 10+1 = 11 */ Говорят, что использование указателя означает отказ от именования (разыменование) адресуемого им объекта. Отказ от именования объектов при наличии возможности доступа по указателю приближает язык Borland C++ по гибкости отображения «объект-память» к языку ассемблера. При вычислении адресов объектов следует учитывать, что идентификатор массива и функции именует переместимую адресную константу (термин ассемблера), или константу типа указатель (термин языка Borland C++). Такую константу можно присвоить переменной типа указатель, но нельзя подвергать преобразованиям: int x[100], *y; y=x; // присваивание константы переменной ... x=y; // Ошибка: в левой части - указатель-константа Указателю-переменной можно присвоить значение другого указателя либо выражения типа указатель с использованием, при необходимости операции приведения типа (приведение необязательно, если один из указателей имеет тип "void *"). int i, *x; char *y; x=&i; /* x -> поле объекта int */ y=(char *)x; /* y -> поле объекта char */ y=(char *)&i; /* y -> поле объекта char */ Рассмотрим фрагмент программы: int a=5, *p, *p1, *p2; p=&a; p2=p1=p; ++p1; p2+=2; printf(“a=%d,p=%d,p=%p,p1=%p, p2=%p.\n”,a,p,p,p1,p2); Результат выполнения: a=5, *p=5, p=FFC8, p1=FFCC, p2=FFD0. Конкретные значения адресов зависят от ряда причин: архитектура компьютера, тип и размер оперативной памяти и т.д. 8.2. Операции над указателями (адресная арифметика) Указатель может использоваться в выражениях вида: p # ie, ##p, p##, p# = ie, где p - указатель, ie - целочисленное выражение, # - символ операции '+' или '-'. Значением таких выражений является увеличенное или уменьшенное значение указателя на величину ie*sizeof(*p). Следует помнить, что операции с указателями выполняются в единицах памяти того типа объекта, на который ссылается этот указатель. Текущее значение указателя всегда ссылается на позицию некоторого объекта в памяти с учетом правил выравнивания для соответствующего типа данных. Таким образом, значение p#ie указывает на объект того же типа, расположенный в памяти со смещением на ie*sizeof(*p) позиций. Разрешается сравнивать указатели и вычислять разность двух указателей. При сравнении могут проверяться отношения любого вида (">",">=","<","<=","==","!="). Наиболее важными видами проверок являются отношения равенства или неравенства. Отношения порядка имеют смысл только для указателей на последовательно размещенные объекты (элементы одного массива). Разность двух указателей дает число объектов адресуемого ими типа в соответствующем диапазоне адресов. Очевидно, что уменьшаемый и вычитаемый 22 указатель также должны соответствовать одному массиву, иначе результат операции не имеет практической ценности. Любой указатель можно сравнивать со значением NULL, которое означает недействительный адрес. Значение NULL можно присваивать указателю как признак пустого указателя. NULL заменяется препроцессором на выражение (void *)0. 8.3. Указатели на указатели В языке Borland C++ можно описать переменную типа «указатель на указатель». Это ячейка оперативной памяти, в которой будет храниться адрес указателя на какую либо переменную. Признак такого типа данных – повторение символа «*» перед идентификатором переменной. Количество символов «*» определяет уровень вложенности указателей друг в друга. При объявлении указателей на указатели возможна их одновременная инициализация. Например: int a=5; int *p1=&a; int **pp1=&p1; int ***ppp1=&pp1; Теперь присвоим целочисленной переменной а новое значение, например 10. Одинаковое присваивание произведут следующие операции: a=10; *p1=10; **pp1=10; ***ppp1=10; Для доступа к области ОП, отведенной под переменную а можно использовать и индексы. Справедливы следующие аналоги: *p1 <-> p1[0] **pp1 <-> pp1[0][0] ***ppp1 <-> ppp1[0][0][0] ТАКИМ ОБРАЗОМ, УКАЗАТЕЛИ НА УКАЗАТЕЛИ – ЭТО ИМЕНА МНОГОМЕРНЫХ МАССИВОВ. Соответствие между указателями и массивами с произвольным числом измерений на примере четырехмерного массива: float name[][][][]; <-> float ****name; В последнем случае эквивалентными являются выражения: name[i][j][k][l] *(*(*(*(name+i)+j)+k)+l) *(*(*(name+i)+j)+k)[l] *(*(name+i)+j)[k][l] *(name+i)[j][k][l] 8.4. Массивы указателей В языке Borland C++ можно использовать массивы указателей, элементы которых содержат, как правило, указатели на строковые данные. Объявляется такой массив, например, так: char *m[5]. Здесь массив m[5] – массив, который может содержать пять адресов данных типа char. Массив указателей можно при объявлении инициализировать, т.е. назначать при объявлении его элементам конкретные адреса заданных строк. В качестве примера приведена программа, формирующая массив указателей с одновременной инициализацией его элементов. Программа распечатывает номер строки, ее адрес и значение. #include <stdio.h> #include <conio.h> void main(void) { int i,k; char *m[]={"Winter", "Spring", "Summer", "Autoumn"}; k=sizeof(m)/sizeof(*m); 23 printf("\n size of array=%d",k); for(i=0; i<k; i++) printf("\n string - %d; adress - %p; string:%s" ,i,m[i],m[i]); getch(); } В результате получим: size of array=4 string - 0; adress - 0042007C; string: Winter string - 1; adress - 00420074; string: Spring string - 2; adress - 0042006C; string: Summer string - 3; adress - 00420064; string: Autoumn Конкретные значения адресов зависят от ряда причин: архитектура компьютера, тип и размер оперативной памяти и т.д Лекция 7. Тема. Функции Цель: Познакомиться с механизмом составления взаимодействия пользовательских функций языка Borland С++. и организации 9.1. Функции пользователя В алгоритмическом языке Borland С++ кроме использования стандартных функций существует возможность работать с функциями пользователя. Предварительно функцию необходимо объявить. Объявление функции пользователя т.е. ее декларация возможна в двух формах – в форме описания и в форме определения (реализации). Описание функции – декларация ее прототипа вначале программного файла. Используется следующий способ декларации функций: <тип_результата> <имя_функции>(<тип> <переменная>, …<тип> <переменная>); Идентификаторы переменных в круглых скобках прототипа указывать не обязательно, так как компилятор языка их не обрабатывает. Пример описания функции fun со списком параметров: float fun(int, float, int, int); Прототип функции сообщает компилятору о том, что далее в тексте программы будет приведено полное определение (полный ее текст). Полное определение функции имеет следующий вид: <тип_результата> <имя_функции>(список параметров) { код функции } Тип результата определяет тип значения, который возвращается функцией в точку ее вызова при помощи оператора возврата return. Если тип функции не указан, то по умолчанию предполагается тип int. Список параметров состоит из перечня типов и имен параметров, разделенных запятыми. Функция может не иметь параметров, но круглые скобки необходимы в любом случае. Оператор return вызывает немедленный выход из данной функции и возврат в вызывающую ее функцию. Этот оператор также используется для возврата результата работы функции. Отметим, что в теле функции может быть несколько операторов return, но может и не быть ни одного. В таких случаях возврат в вызывающую ее функцию происходит после выполнения последнего оператора. Пример функции, определяющей наименьшее из двух целых чисел: int mini(int x, int y) { int t; if (x<y) t=x; else t=y; 24 return t; } Можно написать функцию mini и таким образом: mini(int x, int y) { return (x<y)? x:y; } Если тип возвращаемого результата опущен, то он по умолчанию будет иметь тип int. Все функции, возвращающие значение, должны использоваться в правой части выражений языка Borland С++, иначе возвращаемый результат будет утерян. Но они не могут использоваться в левой части операторов присваивания, за исключением тех случаев, когда возвращается адрес результата работы функции. Если функция не возвращает никакого значения, она должна быть описана как функция типа void (пустая). Например, для вывода горизонтальной строки на экран дисплея можно использовать следующую функцию: void lin(char a) { int k; for(k=0; k<80; k++) printf(“%c”, a); } Если у функции отсутствует список параметров, то при декларации такой функции желательно в круглых скобках также указать ключевое слово void. Например, заголовок основной функции должен выглядеть так: void main(void). В языке Borland С++ каждая функция – это отдельный блок программы, вход в который возможен только через вызов данной функции. Например, нельзя оператором перехода goto передать управление внутрь любой функции. Вызов функции имеет следующий формат: <имя_функции> (список_аргументов) где в качестве аргументов можно использовать константы, переменные, выражения (их значения перед вызовом функции будут компилятором определены). Аргументы списка вызова должны полностью совпадать со списком параметров вызываемой функции по количеству, по порядку следования и по типам соответствующих им параметров. Отметим, что в случае отсутствия аргументов (в заголовке функции отсутствуют параметры) наличие круглых скобок у имени функции в точке ее вызова обязательно. Пример работы с функциями Ввести массив NxN (не больше 50) целых чисел и в функции посчитать сумму его положительных значений. #include <stdio.h> #include <conio.h> void summa(int, int a1[ ][50]); void main(void) { int a[50][50]; int i,j,N; puts("\n Введите размер массива N(<50)\n"); scanf(“%d”,&N); printf("\n Введите данные \n"); for(i=0; i<N; i++) for(j=0; j<N; j++) { printf("\n a[%d][%d]=", i+1, j+1); 25 scanf("%d", &a[i][j]); } summa(N,a); } void summa(int n, int a1[ ][50]) { int i,j,s; puts(" ФУНКЦИЯ summa "); /* ВЫЧИСЛЕНИЕ СУММЫ ПОЛОЖИТЕЛЬНЫХ ЭЛЕМЕНТОВ МАССИВА */ for (s=0,i=0; i<n; i++) { printf("\n"); for (j=0;j<n;j++) if (a1[i][j]>0) s+=a1[i][j]; } printf("\a СУММА = %d, Press any key... ",s); getch(); } Лекция 8. Тема. Классы памяти. Рекурсивные функции Цель: Ознакомиться с областью действия переменных, рассмотреть основные классы памяти и рекурсивные функции. 10.1. Область действия переменных Область действия переменной – это правила, которые устанавливают, какие данные доступны из текущего места программы. Имеются три типа переменных: глобальные, локальные и формальные. Область действия локальных переменных – это те блоки, где локальные переменные объявлены. При выходе из блока локальная переменная и ее значение теряются. Формальные переменные – это параметры в заголовке функции пользователя. Формальные параметры используются в теле функции так же, как локальные переменные. Область действия формальных параметров – блок, являющийся телом функции. Глобальные переменные объявляются вне какой-либо функции. Глобальные переменные могут быть использованы в любом месте программы, но перед их первым использованием они должны быть объявлены и проинициализированы. Область действия глобальных переменных - вся программа с момента их объявления. 10.2. Классы памяти. Рекурсивные функции В языке Borland C++ каждая переменная принадлежит к одному из четырех классов памяти – автоматическая (auto), внешняя (extern), статическая (static), регистровая (register). Тип памяти указывается ключевым словом (auto, extern, static, register), стоящим перед спецификацией типа переменной. Например, register int a; По умолчанию (если класс памяти для переменной не указан) переменная относится к классу auto и будет размещена в стеке. В языке Borland С++ аргументы при стандартном вызове функции передаются по значению, т.е. в функцию передаются не оригиналы аргументов, а их копии. В стеке выделяется место для формальных параметров функции и в это выделенное место при ее вызове заносятся значения фактических аргументов. Затем функция использует и может изменять эти значения в стеке. Но при выходе из функции измененные значения теряются. Вызванная функция не может изменить значения переменных, указанных как фактические аргументы при обращении к данной функции. 26 В случае необходимости, функцию можно использовать для изменения передаваемых ей аргументов. В этом случае в качестве аргумента необходимо в вызываемую функцию передавать не значение переменной, а ее адрес. А для обращения к значению аргумента-оригинала использовать операцию «*». Пример функции, в которой меняются местами значения аргументов x и y: void z1(int *x, int *y) { int t; t=*x; *x=*y; *y=t; } Участок программы с обращением к данной функции: int a=2, b=3; void z1(int*, int*); … printf(“\n a=%d, b=%d”, a, b); z1(&a, &b); printf(“\n a=%d, b=%d”, a, b); … При таком способе передачи в вызываемую функцию аргументов их значения теперь будут изменены, т.е. на экран монитора будет выведено: a=2, b=3 a=3, b=2 В языке Borland С++ функции могут вызывать сами себя. В этом случае функция называется рекурсивной. Пример рекурсивной функции – вычисление факториала числа n!=1*2*3*…*n: #include<stdio.h> #include<conio.h> #include<iostream.h> double factor(double num) { if (num= =1) return 1.; return num*factor(num-1.); } void main() { int arr[100]; //Максимум 100 чисел int size, i; cout<<”Введите количество чисел: ”<<endl;; cin>>size; cout<<“ Введите числа:”<<endl; for(i=0;i<size;i++) cin>>arr[i]; for(i=0;i<size;i++) cout<<”Факториал ”<<arr[i]<<”=”<<factor(arr[i]); getch(); } /* Введите количество чисел: 3 Введите числа: 5 13 17 Факториал 5 = 120 Факториал 13 = 6227020800 Факториал 17 = 355687428096000 */ 27 Лекция 9. Тема. Командная строка. Параметры функции main() Цель: Рассмотреть возможность передачи аргументов головному модулю запущенной на выполнение программы – функции main(), посредством командной строки. Аргументы командной строки - это текст, записанный после имени запускаемого на выполнение .com или .ехе файла, либо передаваемый программе с помощью опции интегрируемой среды Borland C++ - arguments. В качестве аргументов целесообразно передавать имена файлов, функций, текст, задающий режим работы программы, а также сами данные (числа). Borland C++ поддерживает три параметра функции main(). Для их обозначения рекомендуется использовать общепринятые имена argc, argv, envp (но не запрещается использовать любые другие имена). Вход в функцию main при использовании командной строки имеет вид: int main (int argc, char *argv[], char *envp[]) { Тело функции} Либо: int main (int argc, char **argv, char **envp) { Тело функции} Первый параметр (argv) сообщает функции количество передаваемых в командной строке аргументов, учитывая в качестве первого аргумента имя самой выполняемой программы (т.е. количество слов, разделенных пробелами). Отсюда следует, что количество параметров не может быть меньше единицы, так как первый аргумент – имя программы с полным путем к ней присутствует всегда. Второй параметр (char **argv, char *argv[])является указателем на массив их указателей на слова (т.е. сами параметры) из командной строки. Каждый параметр хранится в виде ASCIZ – строки. Под словом понимается любой текст, не содержащий символов пробела или табуляции. Запятые, точки и прочие символы не рассматриваются как разделители. Последним элементом массива указателей является нулевой указатель (NULL). Например, пусть программа L13_5.exe запускается сл. образом: C:\BorlandC\bin\L13_5.exe ddd 123 bcde a+b и заголовок функции main имеет вид: void main(int argc, char *argv[]) {...} тогда argc = 5 и создается массив из пяти указателей, каждый из которых указывает на отдельное слово (аргумент). Третий параметр функции main (char **envp) случит для передачи в программу информации о системном окружении операционной системы, т.е. о среде, в которой выполняется программа. Он является указателем на массив указателей, каждый указатель определяет адрес, по которому записана строка информации о среде, определяемая операционной системой. При знаком конца массива (как и в char *argv[]) является нулевой указатель. Среда, в которой выполняется программа, определяет некоторые особенности поведения оболочки и ядра операционной системы. 11.1. примеры программ с использованием параметров командной строки Ниже приведены программы, демонстрирующие разные способы вывода на экран аргументов командной строки. //пример: вывести параметры командной строки посимвольно. #include<iostream.h> #include<conio.h> #include <stdio.h> void main(int argc, char *argv[]) { int i,j; 28 for(i=0;i<argc;i++) { j=0; while(argv[i][j]) cout<<argv[i][j++]<<” “; } getch(); } /* пример: если в командной строке параметры отсутствуют, то вывести на экран текст: «Командная строка: пустая», иначе вывести на экран текст: «Командная строка:» и далее ее содержимое. */ #include<iostream.h> #include<conio.h> #include <stdio.h> char ss[]=”Командная строка: ”; /*глобальная переменная, она видима в main, но адрес ее будем получать из GetStrn*/ char *GetStrn(void){return ss;} //функция void main(int argc, char *argv[]) { char *s; if (argc > 1) s=argv[1]; else s=”пустая.”; cout<<GetStrn()<<s; // cout<<ss<<s; getch(); } //пример: вывести содержимое командной строки на экран строками. #include<stdio.h> void main(int argc, char **argv) { int i; cout<<”Имя программы:” <<*argv<<endl; for(i=1;i<argc;i++) cout<<” “<<argv[i]; getch() } Лекция 10. Тема. Структуры Цель: Изучить особенности работы с составным типом данных – структуры. Язык программирования Borland C++ поддерживает определяемые пользователем структуры – структурированный тип данных. Он является собранием одного или более объектов (переменных, массивов, указателей, других структур и т.д.), которые для удобства работы с ними сгруппированы под одним именем. Структуры облегчают написание и понимание принципов работы программ, а также помогают сгруппировать данные, объединяемые каким-либо общим понятием (например, данные: имя/фамилия/год рождения/ оценки по экзаменам можно поместить в структуру данных о студенте). Структуры позволяют группу связанных между собой переменных использовать как множество отдельных элементов, а также как единое целое. Как и массив, структура представляет собой совокупность данных, но отличается от него тем, что к ее элементам (компонентам) необходимо обращаться по имени и ее элементы могут быть различного типа. 12.1. Объявления шаблонов структур Общий синтаксис объявления шаблона структуры: struct имя_ шаблона { 29 тип1 имя_переменной1; тип2 имя_переменной2; // другие члены данных; } Структура объединяет логически связанные данные разных типов. Структурный тип данных определяется описанием: struct имя_структуры { описание элементов; }; Для выделения памяти под структуру надо определить структурную переменную: struct имя_структуры имя_переменной; При определении структур можно задавать начальные значения элементам структур. Для ввода значений элементов структур можно использовать оператор cin>> потокового ввода или оператор ввода scanf. Над структурами допускаются следующие операции: 1) Взятие адреса структуры. Адрес структуры может быть получен путем применения операции указатель (&) к структурной переменной. 2) Доступ к элементу структуры можно выполнить с помощью операций: точка (.) прямой доступ или стрелка ( -> ) - доступ по указателю. Структурная переменная может использоваться так же, как и переменные типов float,int,char и т.д. Например: struct date /* объявление шаблона структуры типа date */ { int day,month,year; }; struct person /*Шаблон структуры типа person */ {char fam[30], //Фамилия im[20], //имя otch[15]; //отчество float weight; //вес int height; //рост struct date birthday; //День рождения } Структура date имеет три поля типа int. Шаблон структуры person включает поле birthday, которое, в свою очередь, имеет ранее объявленный тип данных: struct date. Этот элемент (birthday) содержит в себе компоненты шаблона struct date. 12.2. Определение структур-переменных Если описатель структуры записан до размещения всех функций в исходном файле, то он будет доступен каждой из функций в этом файле. При определении структурной переменной можно инициализировать (останавливать значения полям структуры). Например struct date { int day,month,year;}; d[5]={ { 1,3,1980}, { 5,1,1990}, { 1,1,2002} }; 12.3. Размещение структурных переменных в памяти Число байтов, выделенное под структурную переменную, не всегда равно сумме дли отдельных ее элементов из-за некоторых особенностей работы процессора с данными с фиксированной и плавающей точкой, что приводит к так называемому «выравниванию», размещению элементов с четного адреса. 30 При размещении структурной переменной в памяти для выравнивания на границу слова компилятор оставляет между ее элементами и элементами массива структурных переменных пустые байты для соблюдения следующих правил: Структурная переменная (элемент массива структур) начинается на границе слова, т.е. с четного адреса; Любой элемент, кроме элементов типа char, также располагается с четного адреса и соответственно имеет четное смещение от начала структурной переменной; При необходимости в конец структурной переменной добавляется неиспользуемый байт для того, чтобы общее число байтов, занимаемых переменной, было четным. Лекция 11. Тема. Размещение полей битов в структурах Цель: Рассмотреть использование в структурах особого типа полей – полей битов, которые делают возможным доступ к отдельным битам более крупных объектов. Использование полей битов целесообразно в том случае, когда для хранения информации в структуре данных достаточно несколько битов. Общий синтаксис описания битового поля: Тип [имя]: ширина; Рассмотрим каждое поле описания. ПОЛЕ «тип» В Borland C++ для определения содержимого битового поля разрешено использовать тип, интерпретируемый как целый: char, short, int, long ( с модификаторами signed, unsigned), перечисления. В полях типа signed крайний левый бит является знаковым. Например, при объявлении поля типа signed шириной в один байт (например, signed а:1;) оно будет способно хранить только –1 или 0, так как любая ненулевая величина будет восприниматься как (-1) . ПОЛЕ «имя» Ссылка на битовое поле выполняется по имени, указанному в поле «имя». Если имя в данном поле не указано, то запрошенное количество бит будет отведено, но доступ к ним будет невозможен. Разрешается описание поля битов без имени и с шириной равной 0 – в этом случае следующее поле будет начинаться с границы целого. ПОЛЕ «ширина» Каждому полю выделяется точно столько битов, сколько указано в поле «ширина». Данное выражение должно иметь неотрицательное значение. Это значение не может превышать числа разрядов, требуемого для представления значения типа, указанного в поле «тип». Объявление структуры, содержащей поля битов имеет следующий вид: struct имя_структуры { тип [имя 1]:ширина; тип [имя 2]:ширина; тип [имя N]:ширина; }; Рассмотрим примеры объявления битовых полей. Объявление шаблона: struct bitfld1 { int a:2; unsigned b:3; }; Объявление шаблона и определение переменных: struct bitfld2 { int a:1; unsigned b:2; int :4; 31 int c:4; unsigned d:10; } bf1, bf2; 13.1. Размещение полей битов в памяти Поля битов располагаются в машинном слове справа налево, т.е. в направлении от младших разрядов к старшим в очередности их объявления. Если обратиться к общему объявлению структуры с полями битов описанному выше, то размещение полей в памяти можно представить следующим образом: Старшие Разряды машинных слов Младшие Имя N....................................................имя2 имя1 Если общая ширина полей битов в структуре не кратна восьми (т.е. некоторые битовые поля будут не полностью занимать байт), то соответствующее количество битов данного байта будет свободно. Например: struct EXAMPLE { unsigned a:3; int b:2; }; Данная запись обеспечит следующее размещение полей в памяти: Разряды байта Знаковый бит 7 6 5 Не используются 4 3 2 1 0 а b 13.2. Доступ к полям битов. Инициализация полей битов. Недопустимые действия Доступ к полям битов осуществляется так же, как и к другим элементам структурной переменной, т.е. с помощью операции выбора элемента “.”. Например: struct EXAMPLE { int a:2; unsigned int b:3; unsigned int c:6; int d:1; } exam; Доступ к объявленным полям в данном примере происходит следующим образом: Exam.a – доступ к полю “a”; Exam.b – доступ к полю “b” и т.д. При работе с битовыми полями недопустимы: Массивы битовых полей; Указатели на битовые поля; Функции, возвращающие битовые поля. Лекция 12. Объединения Цель: Ознакомиться с понятием объединения, с объявлением, инициализацией объединения. Рассмотреть указатель на объединение, размещение объединений в памяти. Объединение – это особый тип данных языка Borland C++, называемый составным типом. Фактически объединение – это переменная, которая позволяет в разные моменты времени хранить значения различных типов. Объединение может содержать и структурные переменные. Его особенностью является то, что все данные, включенные в объединение, располагаются в памяти с одной и той же границы (адреса), т.е. они перекрывают друг друга. 32 Объединение обычно является частью структуры. Отдельные данные, входящие в объединение, в дальнейшем будем называть полями. 14.1. Объявление объединения Как и любая переменная, объединение должно быть определено. Определение состоит из двух шагов: Задание шаблона объединения; Собственно описание переменной-объединения. В дальнейшем непосредственно переменную – объединение будем называть просто объединением. Шаблон создается при помощи ключевого слова union с использованием следующего синтаксиса: Union [<имя>] {тип поле1; тип поле2; ... тип полеN; }; где <имя> - имя описываемого шаблона, удовлетворяющее правилам задания идентификаторов языка Borland C++; тип – любой тип языка Borland C++; поле1...полеN – имена полей данных, входящих в объединение, удовлетворяющие правилам задания идентификаторов языка Borland C++. Поскольку описание шаблона на самом деле является оператором языка, то в конце ставится точка с запятой. Например, описание простейшего шаблона объединения имеет вид: union EXAMPLE {int i; char ch[2]; }; Поле, являющееся объединением, называют вложенным объединением. Шаблон вложенного объединения должен быть уже известен компилятору. Например: union ENCLOSE {char ch; int im[10]; }; union EXAMPLE {char chm[20]; union ENCLOSE enc1; }enclose_union; 14.2. Инициализация объединения При определении объединения разрешается выполнять его инициализацию. Особенностью инициализации объединения является то, что переменная может быть проинициализирована значением только для первого описанного поля. Например: union EXAMPLE1 { int i; char ch[4]; float f; }; union EXAMPLE1 exam = {12346}; В данном примере вначале был задан шаблон с именем EXAMPLE1, затем по заданному шаблону была определена переменная exam и проинициализирована целым числом 12346, так как первое поле данного объединения имеет тип int. Попытка каким33 либо образом проинициализировать данную переменную значениями для полей ch или f приведет к ошибки. Синтаксис языка Borland C++ позволяет одновременное задание шаблона, определение переменных по данному шаблону и их инициализацию. Например: union EXAMPLE { int i; char ch[2]; }; uni = {9650} 14.3. Указатель на объединение Описание шаблона объединения вводит по существу новый тип данных. Значит, вполне допустимо использовать указатель на введенный тип. Как и любой другой указатель иных типов данных, указатель на объединение занимает либо два байта (near), либо четыре байта (far). Описание указателя на объединение не отличается от описания указателей на другие типы: <типы> *<имя_указателя >; где <тип> - задает имя шаблона объединения, указатель будет данного типа; < имя_указателя > - имя определяемого указателя. Например: union EXAMPLE { int i; char ch[2]; }; *pexam; union EXAMPLE *puni1, *puni2; Указатели на объединение обычно используются для доступа к объединениям, размещенным в динамически выделенной памяти. 14.4. Размещение объединений в памяти Все элементы объединения размещаются в одной и той же области памяти одного и того же адреса. Память, которая выделяется под объединение, определяется размером наиболее длинного из элементов данного объединения. Например, если описать объединение вида union EXAMPLE_1 { int i; float f; double d; char ch; }uni; то компилятор выделит под объединение uni 8 байтов, так как данное объединение содержит поле с максимальной длиной 8 байтов (поле типа double). Если же объявить следующее объединение: union EXAMPLE_2 { long double ld; char let[20]; }uni; то компилятор выделит под объединение uni 20 байтов, так как в этом случае максимальную длину имеет поле, являющееся символьным массивом из элементов, для размещения которго необходимо 20 байтов, в то время как поле типа long double требует всего 10 байтов. Лекция 13. Тема. Динамическое использование памяти. Библиотечные функции Цель: рассмотреть динамическое распределение памяти№ а также библиотечные функции 34 Одним из способов хранения информации является использование системы динамического выделения памяти языка Borland C++. Область свободной памяти, доступной для выделения, находится между областью памяти, где размещается программа. Эта область называется кучей или хипом (от англ. heap – куча). Ядром динамического выделения памяти в Borland C++ являются функции№ объявленные в стандартной библиотеке в заголовочном файле stdlib.h, - malloc(), calloc(), realloc(), free(). В языке Borland C++ есть операция, с помощью которой можно определить размер участка памяти в байтах, который компилятор отводит под массив после его объявления, это операция sizeof. Формат записи: sizeof(параметр); параметр – тип объекта или его идентификатор (только не имя функции). Операция sizeof позволяет определить размер объекта по имени или типу, результатом является размер памяти в байтах (тип результата int). Если указан идентификатор сложного объекта (массив, структура, объединение), то получаем размер всего сложного объекта. Например: sizeof(int) -> 2 байта, int b[5]; sizeof(b) -> 10 байт; int c[3][4]; sizeof(c) -> 24 байта. Операция sizeof применяется при динамическом распределении памяти: float *x; // Указатель массива int n; // Количество элементов массива x=(float*)calloc(n,sizeof(float)); // Захват и очистка памяти Прототипы функций работы с памятью находятся в библиотеке alloc.h, рассмотрим основные из них: void *calloc(unsigned n, unsigned m); - возвращает указатель на начало области памяти для размещения n элементов по m байт каждый, при неудачном завершении возвращает значение NULL; void *malloc(unsigned n); - возвращает указатель на блок памяти длиной n байт, при неудачном завершении возвращает значение NULL; void *realloc(void *bf, unsigned n); - изменяет размер ранее выделенной памяти с адресом начала bf на n байт; void free(void *bf); - освобождает ранее выделенный блок памяти с адресом bf; coreleft(void); - возвращает значение объема неиспользованной памяти (тип возвращаемого результата unsigned – для моделей памяти tiny, small, medium; unsigned long – для других моделей памяти). Приведем пример динамического размещения одномерного массива #include<stdio.h> #include<stdlib.h> #include<conio.h> #include<alloc.h> void main(void) { int i,n; float *a; puts(“\n Input n:”); scanf(“%d”,&n); printf(“\n Свободная память -%d”,coreleft()); a=(float *)calloc(n,sizeof(float));// захват памяти printf(“\n Array a \n”); for(i=0; i<n; i++) { *(a+i)=(float)random(10); // диапазон от 0 до 10 35 printf(“ %d“, a[i]); } printf(“\n Память после захвата -%d”,coreleft()); free(a); // освобождение памяти getch(); } Лекция 14. Тема. Файлы Цель: Изучить способы создания и работы с файлами в языке Borland C++. 16.1. Понятие файла и файловой системы Файл – это набор данных, размещенный на внешнем носителе и рассматриваемый в процессе обработки и пересылке как единое целое. Прежде, чем работать с файлом его нужно открыть для доступа, т.е. создать и инициализировать область данных, которая содержит информацию о файле: имя, путь и т.д. В алгоритмическом языке Borland C++ это делает функция fopen. Она связывает физический файл на носителе, например B:\LR7.CPP, с логическим именем в программе. Логическое имя – это указатель на файл, т.е. на область памяти, где храниться информация о файле. Указатели на файлы необходимо объявлять. Формат объявления такого указателя следующий: FILE *указатель на файл; Например. FILE *f; f=fopen ("B:\LR7.СPP", "w"); Символ "w" определяет право доступа к открываемому файлу. В данном случае открывается файл LR7.СPP на диске B:\ только для чтения. В Borland C++ используются следующие коды, устанавливающие режимы доступа к открываемым файлам: Символ Описание R W A r+ w+ a+ T B Файл открывается только для чтения. Если нужного файла на диске нет, то возникает ошибка. Файл открывается только для записи. Если файла с заданным именем нет, то он будет создан, если же такой файл существует, то перед открытием прежняя информация уничтожается. Файл открывается для дозаписи в его конец новой информации. Файл открывается для редактирования его данных. Возможны и запись, и чтение информации. То же, что и для r+. То же, что и для a, только запись можно выполнять в любое место файла. Доступно и чтение файла. Файл открывается в текстовом режиме. Указывается поле r, w, a, r+, w+, a+. Файл открывается в двоичном режиме. Указывается поле r, w, a, r+, w+, a+. Текстовый режим отличается от двоичного тем, что при открытии файла как текстового пара символов « перевод-строки», «возврат каретки» заменяется на один символ: « перевод-строки» для всех функций записи данных в файл, а для всех функций вывода символ «перевод-строки» теперь заменяется на два символа: « переводстроки»,«возврат каретки». По умолчанию файл открывается в текстовом режиме. 36 После работы с файлом, доступ к файлу необходимо закрыть. Закрывает файл в языке Borland C++ функция fclose. Например, из предыдущего примера ее закрывается так: fclose (f); Для открытия нескольких файлов введена функция, объявленная следующим образом: Void fcloseall(Void); Если требуется изменить режим доступа к файлу, то для этого сначала необходимо закрыть данный файл, а затем вновь его открыть, но с другими правами доступа. Для этого используют стандартную функцию freopen, описанную в stdio.h как FILE* freopen (char filename, chov*mode, FILE *stream). Эта функция сначала закрывает файл, связанный с потоком stream (как это делает функция fopen), а затем открывает файл с именем filename и правами доступа mode, записывая информацию об этом файле в переменную stream. В алгоритмическом языке Borland C++ имеется возможность работы с временными файлами, т.е. с такими, которые нужны только в процессе работы программы и которые надо удалить после выполнения части вычислений. в этом случае используют функцию tmpfile со следующим объявлением: FILE* tmpfile (void). Функция tmpfile создает на диске временный файл с правами доступа «w+b» и возвращает указатель на управляющий блок по типу атрибута FILE. После завершения работы программы или после закрытия временного файла он автоматически удаляется из диска. Все действия по чтению/записи данных в файл можно разделить на три группы: 1. Операции посимвольного ввода-вывода, 2. Операции построчного ввода-вывода, 3. Операции ввода-вывода по блокам. Ниже приведены основные функции, применяемые в каждой из указанных трех групп операций. 16.1.1. Посимвольный ввод-вывод В операциях посимвольного ввода-вывода происходит прием одного символа из файла или передача одного символа в файл. Функция Действие функции int fgets(FILE *fp) Чтение и возврат символа из открытого файла int fgetchov(void) Читает и возвращает символ из файла stdin. int ungetc(int ch, FILE *fp) Возвращает символ ch в файл. Следующая операция чтения символа из файла вернет этот символ. int fputs(int ch, FILE *fp) Записывает в файл код ch символа. 16.1.2. Построчный ввод-вывод В операциях построчного ввода-вывода за один прием происходит перенос из файла (или в файл) строк символов. Функция Действие функции int gets (char *S) Читает байты из файла stdin и записывает их в строку S до тех пор, пока не встретит символ ' \n ', который заменяется на нуль – терминатор. int fgets (char *S int m, FILE *fp) Извлекает байты из файла, описываемого fp, и записывает их в строку S до тех пор, пока не встретит символ ' \n ' или пока не будет считана m байтов. int fputs (char *S, FILE *fp) Записывает в файл байты из строки S до тех пор, пока не встретится нуль-терминатор, который в файл не переносится и на символ ' \n ' не заменяется. int puts (char *S) Записывает в файл, stdout байты из строки S до тех пор, пока не встретится нуль-терминатор, который в файл 37 переносится и заменяется на символ ' \n '. 16.1.3. Блоковый ввод-вывод В операциях блокового ввода-вывода работа происходит с целыми блоками информации. Действие функции Функции int fread (void *ptv, int Считывает n блоков по size байт каждый из файла fp, в область size, int n, FILE *fp) памяти, на которую указывает указатель ptv (необходимо заранее отвести память под считываемый блок). int fwrite (void *ptv, Записывае n блоков по size байт каждый из области памяти, на int size, int n, FILE которую указывает ptv, в открытый файл fp. *fp) Пример работы с файлами. Следующая программа формирует целочисленный бинарный файл, дозаписывает в его окончание новые данные, и выводит сохраненные данные на печать: #include <stdio.h> #include <conio.h> void main(void) { int a=1, b=20, c, d; FILE *in, *out, *add; clrscr(); /* ........ ЗАПИСЬ ЧИСЕЛ В ФАЙЛ ......*/ in=fopen("lr8.dat","wb"); fprintf(in,"%d %d \n",a,b); fclose(in); puts("ЧИСЛА a, b ЗАПИСАНЫ В ФАЙЛ"); puts("Press any key..."); getch(); /* ......... ЧТЕНИЕ ЧИСЕЛ ИЗ ФАЙЛА ........*/ out=fopen("lr8.dat","rb"); fscanf(out,"%d%d", &c, &d); printf("\n a=%d b=%d ", c, d); fclose(out); puts("\n ЧИСЛА ПРОЧИТАНЫ ИЗ ФАЙЛА"); puts("Press any key..."); getch(); /* ......... ДОПОЛНЕНИЕ ФАЙЛА ..........*/ add=fopen("lr8.dat","a"); puts("ВВЕДИТЕ ЧИСЛА ЦЕЛОГО ТИПА c и d"); scanf("%d%d",&c,&d); fprintf(add,"%d %d \n",c,d); printf("\n c=%d d=%d \n ",c,d); fclose(add); puts("ЧИСЛА c и d ДОПИСАНЫ В ФАЙЛ"); puts("Press any key..."); getch(); /* ......... ЧТЕНИЕ ЧИСЕЛ ИЗ ФАЙЛА ........*/ out=fopen("lr8.dat","rb"); fscanf(out,"%d%d%d%d", &a, &b, &c, &d); printf("\n a=%d b=%d ", a, b); printf("\n c=%d d=%d ", c, d); 38 fclose(out); puts("\n РАСШИРЕННЫЙ ФАЙЛ!"); puts("Press any key..."); getch(); } Лекция 15. Тема. Графические возможности языка Borland C++ Цель: Изучение работы дисплея в графическом режиме. 17.1. Графический режим работы При работе в графическом режиме экран дисплея представляет собой матрицу точек (пикселов - pixel) - т.е. матрицу отображаемых точек. При этом число столбцов и строк пикселов (разрешение экрана дисплея) зависит от режима работы видеоадаптера. Можно управлять цветом каждого пиксела, задавая цвета фона, рисунка и заполнения замкнутых областей экрана дисплея, а также создавать эффект движения изображений. За начало координат экрана дисплея в графическом режиме принимается верхний левый угол с координатами x=0 и y=0, где x - координата по горизонтали, y - координата по вертикали точки ( пиксела). Во всех примерах программ, приведенных далее по тексту, нулевые координаты присваиваются верхнему левому углу создаваемого графического окна. Содержимое библиотеки графических функций в алгоритмическом языке Borland C++ подразделяется на немобильную группу функций (функции зависят от типа адаптера) и на мобильную группу функций. Немобильная группа графических программ представляет собой BGI драйвер (Borland Graphics Interface). Драйвер - это обработчик прерывания 10h, он должен дополнить системный обработчик до того, как будут использоваться мобильные графические функции. Перед завершением программы таблица векторов прерываний восстанавливается. Основные функции BGI-драйвера - установка и обновление ряда внешних переменных, которые могут изменяться как функциями системного обработчика прерывания 10h (например, при переключении видеорежима или при изменении регистров палитры), так и мобильными функциями библиотеки графики алгоритмического языка Turbo C (TC) или Borland C++. Для различных типов адаптеров применяются различные драйверы: CGA.BGI - драйвер для CGA и MCGA, EGAVGA.BGI - драйвер для адаптеров EGA,VGA, HERC.BGI - драйвер для монохромных адаптеров Hercules. Графические функции мобильной группы подразделяются на 1. Функции для подготовки графической системы и перехода в текстовый режим. 2. Функции для получения изображений на экране дисплея. 3. Функции для установки параметров изображения( вид штриховки, толщина линий и т.д.). 4. Функции для определения параметров режимов и изображений. 17.2. Функции для подготовки графической системы Перед использованием графических функций необходимо инициализировать систему графики. Графические режимы, поддерживаемые библиотекой графики, задаются символическими константами, описанными в файле <graphics.h> в перечислимом типе graphics_mode. Инициализация графической системы производится функцией initgraph(), которая загружает графический драйвер и переключает экран дисплея в требуемый графический режим. Прототип функции initgraph: initgraph(&g_driver,&g_mode," "); В двойных апострофах (третий параметр в прототипе функции) необходимо указать путь (маршрут) к графическому драйверу. Если указать пробел, то графический драйвер 39 должен быть в текущем каталоге. Первый параметр - &g_driver - тип графического драйвера:1 - CGA, 3 - EGA, 9 - VGA и т.д. Второй параметр - &g_mode - графический режим ( рассмотрим только для VGA драйвера): VGA 0 640x200 VGAMED 1 640x350 VGAHI 2 640x480 Запись типа 640x200 - это разрешающая способность экрана дисплея в графическом режиме (число строк умножить на число столбцов). Для задания автоматического режима графики необходимо записать: int g_diver=DETECT,g_mode; Для завершения работы в графическом режиме необходимо применить функцию closegraph(); 17.3. Основные функции для получения изображения 1. Вычерчивание окружности: circle(x,y,r); 2. Вычерчивание закрашенного прямоугольника: bar(x1,y1,x2.y2); 3. Вычерчивание параллелепипеда: bar3d(x1,y1,x2,y2,глубина,p); p=0 или p=1 - верхняя грань отображается (не отображается) 4. Вычерчивание линии: line(x1,y1,x2,y2); 5. Вычерчивание точки: putpixel(x,y,цвет); 6. Вычерчивание прямоугольника: rectangle(x1,y1,x2,y2); 7. Вывод текста: outtext(x,y,"текст"); 8. Установка указателя на экране дисплея: moveto(x,y); 9. Очистка экрана дисплея: cleardevice(void); 10. Заполнение ранее заданным наполнителем замкнутой области: floodfill(x,y,c); c - номер цвета линии, ограничивающей область. 17.4. Основные функции для установки параметров изображения 1. Установка цвета линий: setcolor(n); 2. Установка цвета фона: setbkcolor(n); 3. Установка стиля наполнителя замкнутых линий: setfillstyle(номер наполнителя 0 - 12 ,цвет); 4. Установка толщины линий: setlinestyle(стиль линии,0,толщина); 0 - непрерывная, 1 - из точки, 2,3 - штрих, 5. Установка стиля текста: settextstyle(шрифт 0-4, направление: 0 – горизонтальное, 1 вертикальное, размер 1-0); 3. ПРАКТИЧЕСКИЕ И ЛАБОРАТОРНЫЕ ЗАНЯТИЯ Лабораторная работа Тема: Структура простейшей программы. Построение блок-схем линейных процессов. Использование условных операторов. Форматированный вывод на экран. Форматированный ввод с клавиатуры Цель: Развитие навыков по составлению блок-схем, умение применять линейные и разветвляющие алгоритмы. Существует несколько способов записи алгоритмов. Алгоритм может быть записан на естественном языке в виде блок-схемы на алгоритмическом языке 40 Однако блок-схемы, наверно, самый удобный и наглядный способов записи алгоритмов. Любая команда алгоритма записывается в блок-схеме в виде графического элемента - блока, и дополняется словесным описанием. Блоки в блок-схемах соединяются линиями потока информации. Направление потока информации указывается стрелкой. В случае потока информации сверху вниз и слева направо стрелку ставить не обязательно. Блоки в блок-схеме имеют только один вход и один выход (за исключением логического блока). Перечень основных элементов блок-схем Элеме нт блокНаименование Содержание схемы Блок вычислений (вычислительный блок) Вычислительные действия или последовательность действий Логический блок (блок условия) Выбор направления выполнения алгоритма в зависимости от некоторого условия Блок ввода-вывода данных Общее обозначения ввода (вывода) данных (вне зависимости от физического носителя) Начало (конец) Начало или конец алгоритма, вход или выход в подпрограмме Процесс пользователя (подпрограмма) Вычисление по стандартной программе или подпрограмме Блок модификации Функция выполняет действия, изменяющие пункты (например, заголовок цикла) алгоритма Соединитель Указание связи прерванными линиями между потоками информации в пределах одного листа Межстраничные соединения Указание связи между информацией на разных листах Базовые структуры алгоритмов — это определенный набор блоков и стандартных способов их соединения для выполнения типичных последовательностей действий. К основным структурам относятся следующие: линейные разветвляющиеся циклические Линейными называются алгоритмы, в которых действия осуществляются последовательно друг за другом. Стандартная блок-схема линейного алгоритма приводится ниже: 41 Разветвляющимся называется алгоритм, в котором действие выполняется по одной из возможных ветвей решения задачи, в зависимости от выполнения условий. В отличие от линейных алгоритмов, в которых команды выполняются последовательно одна за другой, в разветвляющиеся алгоритмы входит условие, в зависимости от выполнения или невыполнения которого выполняется та или иная последовательность команд (действий). В качестве условия в разветвляющемся алгоритме может быть использовано любое понятное исполнителю утверждение, которое может соблюдаться (быть истинно) или не соблюдаться (быть ложно). Такое утверждение может быть выражено как словами, так и формулой. Таким образом, алгоритм ветвления состоит из условия и двух последовательностей команд. В зависимости от того, в обоих ветвях решения задачи находится последовательность команд или только в одной разветвляющиеся алгоритмы делятся на полные и не полные (сокращенные). Стандартные блок-схемы разветвляющегося алгоритма приведены ниже: Структура программы на языке C++ Программа на языке C++ состоит из функций, описаний и директив. Одна из функций должна иметь имя main. Выполнение программы начинается с первого оператора этой функции. Простейшее определение функции имеет следующий формат: тип_возвращаемого_значения имя ( [ параметры ] ) {операторы, составляющие тело функции} Как правило, функция используется для вычисления какого-либо значения, поэтому перед именем функции указывается его тип. О функциях поговорим позже. приведем самые необходимые сведения: · Если функция не должна возвращать значение, указывается тип void. · Тело функции заключается в фигурные скобки. · Функции не могут быть вложенными. · Каждый оператор заканчивается точкой с запятой (кроме составного оператора). Программа может состоять из нескольких модулей (исходных файлов) и, как правило, содержит директивы препроцессора. Пример структуры программы, содержащей функции main, f1 и f2]: директивы препроцессора описания void main() { операторы главной функции } int f1() { операторы функции f1 } int f2() { операторы функции f2 } Директивы препроцессора С увеличением объема программы становится неудобно хранить ее в одном файле. Разбиение программы на функции является первым шагом в повышении уровня 42 абстракции программы, следующий – группировка функций и связанных с ними данных в отдельные файлы (модули), компилируемые раздельно. Получившиеся в результате компиляции объектные модули объединяются в исполняемую программу с помощью компоновщика. Разбиение на модули уменьшает время перекомпиляции и облегчает процесс отладки. Чем более независимы модули, тем легче отлаживать программу. Модуль содержит данные и функции их обработки. Для того чтобы использовать модуль, нужно знать только его интерфейс. Интерфейсом модуля являются заголовки всех функций и описания доступных извне типов, переменных и констант. Описания глобальных программных объектов во всех модулях программы должны быть согласованы. Модульность в языке С++ поддерживается с помощью директив препроцессора, пространств имен, классов памяти, исключений и раздельной компиляции. Препроцессором называется первая фаза компилятора. Инструкции препроцессора называются директивами. Они должны начинаться с символа #, перед которым в строке могут находиться только пробельные символы. Директива #include Директива #include <имя_файла> вставляет содержимое указанного файла в ту точку исходного файла, где она записана. Включаемый файл также может содержать директивы #include. Поиск файла, если не указан полный путь, ведется в стандартных каталогах включаемых файлов. Вместо угловых скобок могут использоваться кавычки (" ") – в этом случае поиск файла ведется в каталоге, содержащем исходный файл, а затем уже в стандартных каталогах. Директива #include является простейшим средством обеспечения согласованности объявлений в различных файлах, включая в них информацию об интерфейсе из заголовочных файлов. Заголовочные файлы обычно имеют расширение .h и могут содержать: · определения типов, констант, встроенных функций, шаблонов, перечислений; · объявления функций, данных, имен, шаблонов; · пространства имен; · директивы препроцессора; · комментарии. В заголовочном файле не должно быть определений функций и данных. При указании заголовочных файлов стандартной библиотеки расширение .h можно опускать. Для каждого файла библиотеки С с именем <name.h> имеется соответствующий файл библиотеки С++ <cname>. Директива #define Директива #define определяет подстановку в тексте программы. Она используется для определения: · символических констант. Формат определения символической константы: #define имя текст_подстановки /* Все вхождения имени заменяются на текст подстановки */ · макросов, которые выглядят как функции, но реализуются подстановкой их текста в текст программы. Формат определения макроса: #define имя( параметры ) текст_подстановки · символов, управляющих условной компиляцией. Они используются вместе с директивами #ifdef и #ifndef. Формат: #define имя Пример. 1 #define M 1000 #define Vasia “Василий Иванович” 43 #define MAX(a,b) ((x)>(y)?(x):(y)) #define __cplusplus Параметры используются при макроподстановке, например, если в тексте программы используется вызов макроса y=MAX(sum1, sum2);, он будет заменен на y=((sum1)>(sum2)?(sum1):(sum2)); Использования макросов и символических констант в программах следует избегать. Предварительные замечания о функциях ввода/вывода В языке нет встроенных средств ввода/вывода. Он осуществляется с помощью подпрограмм, типов и объектов, содержащихся в стандартных библиотеках ANSI C <stdio.h> и C++ <iostream.h>. Основные функции ввода/вывода в стиле языка C: int scanf (const char* format...)// ввод int printf(const char* format...)// вывод Они выполняют форматированный ввод и вывод произвольного количества величин в соответствии со строкой формата format. Строка формата содержит символы, которые при выводе копируются в поток (на экран) или запрашиваются из потока (с клавиатуры) при вводе, и спецификации преобразования, начинающиеся со знака %, которые при вводе и выводе заменяются конкретными величинами. Типы спецификаторов представлены в табл 1. Таблица 1. Спецификаторы формата для функции printf() Кроме того, к командам формата могут быть применены модификаторы l и h (табл. 2). Таблица 2. Модификаторы формата 44 В спецификаторе формата, после символа % может быть указана точность (число цифр после запятой). Точность задаётся следующим образом: %.n<код формата>. Где n – число цифр после запятой, а <код формата> – один из кодов приведённых выше. Например, если имеется переменная x=10.3563 типа float и необходимо вывести её значение с точностью до 3-х цифр после запятой, то надо написать: printf(«Переменная x = %.3f»,x); В результате получится: Переменная x = 10.356 Можете также указать минимальную ширину поля отводимого для печати. Если строка или число больше указанной ширины поля, то строка или число печатается полностью. Например, если написано: printf("%5d",20); то результат будет следующим: 20 Следует обратить внимание на то, что число 20 напечаталось не с самого начала строки. Если вы надо, чтобы неиспользованные места поля заполнялись нулями, то нужно поставить перед шириной поля символ 0: printf("%05d",20); Результат: 00020 Кроме спецификаторов формата данных в управляющей строке могут находиться управляющие символы (табл. 3). Таблица 3. Управляющие символы Чаще всего используется символ \n. С помощью этого управляющего символа можно переходить на новую строку. Пример 2 #include <stdio.h> int main( { int i; printf("Введите целое число\n"); scanf("%d", &i); printf("Вы ввели число %d, спасибо!", i); } Первая строка программы, приведённой в примере 1, – директива препроцессора, по которой в текст программы вставляется заголовочный файл <stdio.h>, содержащий 45 описание использованных в программе функций ввода/вывод. Все директивы препроцессора начинаются со знака #. Третья строка – описание переменной целого типа с именем i. Функция printf в четвертой строке выводит приглашение ≪Введите целое число≫ и переходит на новую строку в соответствии с управляющей последовательностью \n. Функция scanf заносит введенное с клавиатуры целое число в переменную i, а следующий оператор выводит на экран указанную в нем строку, заменив спецификацию преобразования на значение этого числа. Ввод/вывод в стиле языка C рассмотрен в разделе ≪Функции ввода/вывода≫. Пример 3. Вывести на экран: a=5, b=6, c=9 Код программы: #include <stdio.h> #include <conio.h> void main() { int a,b,c; // Объявление переменных a,b,c a=5; b=6; c=9; printf("a=%d, b=%d, c=%d",a,b,c); getch(); } Блок-схема программы: В примере 3 отражены основные особенности структуры программ на C++, а так же синтаксис и структура. Первые две строки отражают директивы препроцессора, в заголовочных файлах которого подключаются библиотеки. Далее показано объявление основной функции main и её тело, заключённое в фигурные скобки. Тип результата данной функции безличный, обозначенный зарезервированным словом void и пустой список аргументов, обозначенный пустыми круглыми скобками после названия функции: «()». Следует отметить, что главная функция в C++ всегда носит название main. После открытой фигурной скобки расположено объявление переменных. При этом сначала записан тип переменных (int), а далее через запятую перечислены все переменные, относящиеся к данному типу. Запись оператора завершается символом;, который считается составной частью оператора. 46 Для вывода результата на экран используется функция printf. При этом записан спецификатор %d для вывода на экран целого десятичного числа. Функция getch используется для того, чтобы результат работы программы оставался на экране до нажатия любой клавиши. Программа завершается знаком}. Как выглядит программа, представленная в примере 2, с использованием библиотеки классов C++ <iostream.h>имеет вид, показанный в примере 4. Пример 4 #include <iostream.h> int main() { int i; cout << "Введите целое число\n"; cin >> i; cout << "Вы ввели число << i << ", спасибо!"; } Математическая функция x |x| ex xy ln(x) lg10(x) sin(x) cos(x) tg(x) arcsin(x) arccos(x) arctg(x) arctg(x/y) sh(x)=1/2 (ex-e-x) ch(x)=1/2 (ex+e-x) tgh(x) Остаток от деления x на y Наименьшее целое, которое >=x Наибольшее целое, которое <=x Имя функции в языке Borland C++ sqrt(x) fabs(x) exp(x) pow(x,y) log(x) log10(x) sin(x) cos(x) tan(x) asin(x) acos(x) atan(x) atan2(x,y) sinh(x) cosh(x) tanh(x) fmod(x,y) ceil(x) floor(x) Пример: Ввести два ненулевых числа. Найти их сумму, разность, произведение и частное. Вывести полученные значения. Блок-схема: 47 Программа: #include <stdio.h> #include <conio.h> void main() { float a,b; clrscr(); printf("Введите первое число: "); scanf("%f",&a); printf("Введите второе число: "); scanf("%f",&b); printf("Результат:\n"); printf("%.2f+%.2f=%.2f\n",a,b,a+b); printf("%.2f-%.2f=%.2f\n",a,b,a-b); printf("%.2f*%.2f=%.2f\n",a,b,a*b); printf("%.2f/%.2f=%.2f\n",a,b,a/b); getch(); } Задания для самостоятельного выполнения: 1. Найти периметр и площадь прямоугольного треугольника. Ввести длины его катетов a и b. Вывести полученные значения. 2. Ввести длину ребра куба. Найти площадь грани, площадь полной поверхности и объем этого куба. Вывести полученные значения. 3. Найти длину окружности и площадь круга заданного радиуса R. В качестве значения Pi использовать 3.14. Вывести полученные значения. 4. Найти площадь кольца, внутренний радиус которого равен R1, а внешний радиус равен R2 (R1 < R2). В качестве значения Pi использовать 3.14. Ввести радиусы R1 и R2. Вывести полученное значение. 5. Ввести длину окружности. Найти площадь круга, ограниченного этой окружностью. В качестве значения Pi использовать 3.14. Вывести полученное значение. 6. Ввести площадь круга. Найти длину окружности, ограничивающей этот круг. В качестве значения Pi использовать 3.14. Вывести полученное значение. 7. Ввести длину и ширину прямоугольника. Найти его площадь и периметр. Вывести полученные значения. 8. Ввести два положительных числа a и b (a>b). Определить на сколько первое число больше второго и во сколько раз первое число больше второго. Результаты вывести на экран. Разветвляющие алгоритмы 48 Пример: Написать программу решения квадратного уравнения. Проверять действительно ли уравнение квадратное (коэффициент при старшей степени не равен нулю). Блок-схема: Программа: #include <stdio.h> #include <conio.h> #include <math.h> void main() { float a,b,c; // Коэффициенты квадратного уравнения float d; // Дискриминант float x1,x2; // Корни квадратного уравнения clrscr(); printf("Программа решения квадратного уравнения\n"); printf("Введите коэффициенты \n"); printf("a=");scanf("%f",&a); printf("b=");scanf("%f",&b); printf("c=");scanf("%f",&c); printf("Решение: \n"); if(a!=0) { d=pow(b,2)-4*a*c; if(d<0) printf("Уравнение не имеет корней"); else if(d>0) { x1=((-b+sqrt(d))/(2*a)); x2=((-b-sqrt(d))/(2*a)); printf("x1=%.2f\n",x1); printf("x2=%.2f\n",x2); } else { x1=-b/(2*a); printf("x=%.2f\n",x1); 49 } } else printf("Уравнение не является квадратным"); getch(); } Задания для самостоятельного выполнения: 1. Выяснить, принадлежит ли точка с координатами (x, y) кругу радиуса r с центром в начале координат. 2. Дан номер некоторого года (положительное целое число). Вывести число дней в этом году, учитывая, что обычный год насчитывает 365 дней, а високосный — 366 дней. Високосным считается год, делящийся на 4, за исключением тех годов, которые делятся на 100 и не делятся на 400 (например, годы 300, 1300 и 1900 не являются високосными, а 1200 и 2000 — являются). Результат вывести на экран. 3. Дано целое число, лежащее в диапазоне от –99 до 99. Вывести строку — словесное описание данного числа вида "отрицательное двузначное число", "нулевое число", "положительное однозначное число" и т.д. 4. Арифметические действия над числами пронумерованы следующим образом: 1 — сложение, 2 — вычитание, 3 — умножение, 4 — деление. Дан номер действия и два числа A и B (В не равно нулю). Выполнить над числами указанное действие и вывести результат. 5. Даны действительные числа x, y, z. Вывести на печать максимальное из чисел x, y, z. 6. Даны действительные числа x, y, z. Вывести на печать минимальное из чисел x, y, z. 7. Даны действительные числа x, y, z. Вывести на печать минимальное и максимальное из чисел x, y, z. 8. Даны действительные числа x, y, z. Удвоить эти числа, если x>y>z, и заменить их абсолютными значениями, если условия не выполняются. 9. Дано действительное число x. Вычислить z по одной из формул и вывести на печать: z = -x, если x<0, z=x, в противном случае. 10. Даны два действительных числа. Заменить первое число нулем, если оно меньше или равно второму, и оставить числа без изменения в противном случае. 11. Даны положительные действительные числа x, y, z. Выяснить, существует ли треугольник с длинами сторон x, y, z, и напечатать соответствующее сообщение. 12. Даны положительные действительные числа x, y. Выяснить, существует ли треугольник с углами x, y, и если существует, то является ли он прямоугольным. Напечатать соответствующее сообщение. Лабораторная работа Тема: Программирование циклических алгоритмов. Цикл с предусловием. Цикл с постусловием. Программирование циклических алгоритмов. Цикл с параметром. Цель: Развитие навыков по составлению блок-схем, умение применять циклические алгоритмы. Циклическим называется алгоритм, в котором некоторая часть операций (тело цикла — последовательность команд) выполняется многократно. Однако слово «многократно» не значит «до бесконечности». Организация циклов, никогда не приводящая к остановке в выполнении алгоритма, является нарушением требования его результативности — получения результата за конечное число шагов. Перед операцией цикла осуществляются операции присвоения начальных значений тем объектам, которые используются в теле цикла. В цикл входят в качестве базовых следующие структуры: 50 блок проверки условия блок, называемый телом цикла Существуют три типа циклов: Цикл с предусловием Цикл с постусловием Цикл с параметром (разновидность цикла с предусловием) Если тело цикла расположено после проверки условий , то может случиться, что при определенных условиях тело цикла не выполнится ни разу. Такой вариант организации цикла, управляемый предусловием, называется циклом c предусловием. Возможен другой случай, когда тело цикла выполняется по крайней мере один раз и будет повторяться до тех пор, пока не станет ложным условие. Такая организация цикла, когда его тело расположено перед проверкой условия, носит название цикла с постусловием. Цикл с параметром является разновидностью цикла с предусловием. Особенностью данного типа цикла является то, что в нем имеется параметр, начальное значение которого задается в заголовке цикла, там же задается условие продолжения цикла и закон изменения параметра цикла. Механизм работы полностью соответствует циклу с предусловием, за исключением того, что после выполнения тела цикла происходит изменение параметра по указанному закону и только потом переход на проверку условия. Стандартные блок-схемы циклических алгоритмов приведены ниже: Следует отметить, что во всех типах цикла в С++ указывается условие ПРОДОЛЖЕНИЯ цикла. Пример: Задача 1: Найти максимальный элемент из десяти целых чисел, вводимых с клавиатуры. #include <stdio.h> #include <conio.h> void main() { float max,a; int i; clrscr(); for(i==1;i<=10;i++) { printf("chislo %i: ",i); scanf("%f",&a); if(i==1 || max<a) max=a; } printf("max=%.2f",max); getch(); } Задача 2: Найти номер первого минимального элемента во вводимой с клавиатуры последовательности чисел. Условие окончания ввода – ввод числа 0. 51 #include <stdio.h> #include <conio.h> void main() { float min,a; int min_n,i=0; clrscr(); do { i++; printf("chislo %i: ",i); scanf("%f",&a); if(a!=0 && (i==1 || min>a)) { min=a; min_n=i; } } while(a!=0); if(i!=1) printf("min=%.2f ego №=%i",min,min_n); else printf("ne vvedeno chisel"); getch(); } Некоторые стандартные операции Си++: Символ Операция Пример Значение Сложение 3+5 8 + Вычитание 43 - 25 18 Умножение 4*7 28 * Деление 9/2 4 / Остаток при 20 % 6 2 % делении нацело (Обратите внимание, что в приведенной таблице операция деления "/" применялась к двум целым числам, поэтому результат – тоже целое число.) Некоторые логические операции. Символ Операция Пример Значение меньше, чем 3<5 TRUE (истина) < меньше или 43 <= 25 FALSE (ложь) <= равно больше, чем 4>7 FALSE > больше или 9 >= 2 TRUE >= равно равно 20 == 6 FALSE == не равно 20 != 6 TRUE != Логическое И 5 > 2 && 6 > 10 FALSE && Логическое ИЛИ 5 > 2 || 6 > 10 TRUE || Задачи для самостоятельного выполнения 1. Найти номер первого минимального элемента из 10 вводимых с клавиатуры чисел 52 2. Найти номер первого максимального элемента во вводимой с клавиатуры последовательности чисел. Условие окончания ввода – ввод числа 0. 3. Даны два целых числа A и B (A < B). Вывести все целые числа, расположенные между данными числами (включая сами эти числа), в порядке их возрастания. Использовать цикл for. 4. Дано вещественное число A и целое число N (> 0). Вывести все целые степени числа A от 1 до N. Использовать цикл for. 5. Дано вещественное число A и целое число N (N > 0). Вывести A в степени N: AN =A•A•...•A (числа A перемножаются N раз). Использовать цикл for. 6. Написать программу, вычисляющую факториал введенного числа. Использовать цикл for. 7. Найти сумму целых четных чисел, расположенных между вводимыми с клавиатуры числами A и B (A>0, B>0, A<B) Лабораторная работа Тема: Массивы и строки. Многомерные массивы. Работа с символьными строками. Цель: ознакомиться с основными принципами работы с одномерными и двумерными массивами, изучение символьных и строковых переменных и способов их обработки в языке Си. Массив — это структура однотипных элементов, занимающих непрерывную область памяти. С массивом связаны следующие его свойства: имя, тип, размерность, размер. Формат описания массива следующий: тип элементов имя [константное_выражение]. Константное выражение определяет размер массива, т. е. число элементов этого массива. В отличие от Паскаля в Си нельзя определять произвольные диапазоны для индексов. Размер массива, указанный в описании, всегда на единицу больше максимального значения индекса. Размер массива может явно не указываться, если при его объявлении производится инициализация значений элементов. Многомерные массивы Двумерный массив трактуется как одномерный массив, элементами которого является массив с указанным в описании типом элементов. Оператор float R[5][10]; объявляет массив из пяти элементов, каждый из которых есть массив из десяти вещественных чисел. Отдельные величины этого массива обозначаются именами с двумя индексами: R[0] [0], R[0][l], ..., R[4][9]. Объединять индексы в одну пару скобок нельзя, т.е. запись R[2,3] ошибочна. Пример описания трехмерного массива: double X[3] [7] [20]; Как и в Паскале, порядок расположения элементов многомерного массива в памяти такой, что прежде всего меняется последний индекс, затем предпоследний и т.д., и лишь один раз пробегает свои значения первый индекс. При описании многомерных массивов их также можно инициализировать. В языках Си/Си++ нет специально определенного строкового типа данных, как в Турбо Паскале. Символьные строки организуются как массивы символов, последним из которых является символ \0, внутренний код которого равен нулю. Отсюда следует одно важное преимущество перед строками в Турбо Паскале, где размер строки не может превышать 255 (длина указывается в первом байте), — на длину символьного массива в Си нет ограничения. 53 Строка описывается как символьный массив. Например: char STR[20] ; Одновременно с описанием строка может инициализироваться. Возможны два способа инициализации строки — с помощью строковой константы и в виде списка символов. Обработка строк обычно связана с перебором всех символов от начала до конца. Признаком конца такого перебора является обнаружение нулевого символа. Использование строк в качестве параметра функции аналогично рассмотренному выше использованию массивов других типов. Необходимо помнить, что имя массива есть указатель на его начало. Однако для строк имеется одно существенное отличие от массивов других типов: имя строки является указателем-переменной, и, следовательно, его значение может подвергаться изменению. Стандарт Си рекомендует в заголовках функций, работающих со строками, явно использовать обозначение символьного указателя. Пример 1. Задан одномерный массив S, состоящий из десяти элементов вещественного типа. Вывести на экран дисплея значения элементов этого массива в обратном порядке. #include <stdio.h> main() {float s[10]; int i; for (i=0;i<10;i++) scanf("%f",&s[i]); /*ввод элементов массива*/ for (i=9;i>=0;i--) printf("%f",s[i]); /* вывод элементов в обратном порядке*/ } Пример 2. Задана двумерная матрица а, имеющая пять строк и пять столбцов. Определить номер строки с наибольшим числом единиц в этой строке. #include <stdio.h> main() {int i,j,p,q=0,f=0,a[5][5]; for (i=0;i<5;i++) for (j=0;j<5;j++) scanf("%d", &a[i][j]); /*ввод матрицы*/ /*поиск в матрице а строки с наибольшим числом единиц*/ for (i=0;i<5;i++) {p=0; for (j=0;j<5;j++) if (f[i][j]==1) p++; if (q<p) {q=p; f=i;} } /*f - номер строки с наибольшим числом единиц, q - число единиц в f-й строке*/ printf("%d %d",f,q); } Пример 3. Переставить местами элементы главной и побочной диагоналей массива D(6,6). Полученную матрицу вывести на экран дисплея. #include <stdio.h> main() {int i,j,a,d[6][6]; for (i=0;i<6;i++) for (j=0;j<6;j++) 54 scanf("%d", &d[i][j]); /*ввод матрицы*/ /*перестановка местами элементов главной и побочной диагоналей*/ for (i=0; i<5; i++) {a=d[i][i]; d[i][i]=d[i][6-i]; d[i][6-i]=a; } for (i=0; i<6; i++) for (j=0; j<6; j++) printf("%d%c", d[i][j], (j==5)?"\n":" "); /*вывод по строкам элементов матрицы*/ } При выводе элементов матрицы по строкам применена тернарная операция. Смысл этой операции сводится к следующему: если j=5 (закончен вывод элементов по строке), то курсор переводится в начало следующей строки (работает символьная константа "\n"), в противном случае выводится один пробел. Так как в операторе вывода используется символьная константа, то применена спецификация %c. В заключении этого раздела отметим, что массив можно инициализировать, т.е. присвоить его элементам начальные значения. Это делается при объявлении типа массива, например: int a[5]= { 0, 0, 0, 0, 0}; Это значит, что все элементы массива получают нулевое значение. Двумерный массив можно инициализировать следующим образом: int a[3][3] = {{10,20,30}, {40,50,60}, {70,80,90}}; При инициализации число элементов можно не указывать, т.к. в этом случае оно может быть вычислено по количеству присваиваемых значений (в фигурных скобках), например: int a[] = {10,20,30,40,50}; Задания для самостоятельного решения 1. В квадрантной матрице размера r все внешние элементы сдвинуть на один по часовой стрелке. 2. В массиве A из 10 элементов, заполненном случайными числами от -5 до 5, заменить отрицательные элементы нулями. Вывести исходный и полученный массивы. 3. В массиве A из 10 элементов, заполненном случайными числами от -5 до 5, найти и вывести сумму элементов с нечетными индексами. Индекс 0 считать четным. 4. В массиве A из 10 элементов, заполненном случайными числами от -5 до 5, найти и вывести произведение элементов с четными индексами. Индекс 0 считать четным. 5. В массиве A из 10 элементов, заполненном случайными числами от -5 до 5, заменить знак у всех положительных элементов на обратный. 6. В массиве A из 10 элементов, заполненном случайными числами от -5 до 5, увеличить положительные элементы на 2, отрицательные элементы уменьшить на 1, а нулевые оставить без изменения. 7. Задан массив A из 10 элементов, заполненном случайными числами от -5 до 5. 8. формировать массив В из положительных элементов массива А. Оба массива вывести на экран. 9. Задан массив A из 10 элементов, заполненном случайными числами от -5 до 5. 10. Сформировать массив В из отрицательных элементов массива А. Оба массива вывести на экран. 11. Задан массив A из 10 элементов, заполненном случайными числами от -5 до 5. Удалить из массива нулевые элементы. Исходный и полученный массивы вывести на экран. 55 12. Задан массив A из 10 элементов, заполненном случайными числами от -5 до 5. 13. Подсчитать число x, равное среднему арифметическому всех элементов и разделить каждый элемент исходного массива на x. Исходный и полученный массивы вывести на экран. 14. Задан массив A из 10 элементов, заполненном случайными числами от -5 до 5. 15. Подсчитать количество элементов массива, кратных трем. Исходный массив и результаты вычисления вывести на экран. Символы и строки 1. Ввести строку, состоящую из нескольких слов, разделенных одним или несколькими пробелами. Подсчитать количество слов в строке. 2. Ввести строку, состоящую из нескольких слов, разделенных одним или несколькими пробелами. Найти размер самого длинного слова в строке. 3. Ввести строку, состоящую из нескольких слов, разделенных одним или несколькими пробелами. Проверить содержит ли строка введенное слово. 4. Ввести строку, состоящую из нескольких слов, разделенных одним или несколькими пробелами. Найти размер самого короткого слова в строке. Сортировка 1. Сортировка одномерного массива по возрастанию методом прямого выбора 2. Сортировка одномерного массива по возрастанию методом прямого обмена 3. Поиск минимального элемента массива 4. Поиск максимального элемента массива 5. Поиск номера первого минимального элемента массива 6. Поиск номера последнего минимального элемента массива 7. Поиск номера первого максимального элемента массива 8. Поиск номера последнего максимального элемента массива 9. Линейный поиск номера элемента массива с заданным значением 10. Бинарный поиск (метод дихотомии) номера элемента массива с заданным значением Лабораторная работа Тема: Функции. Формат определения функции. Вызов функции. Прототипы функций. Цель: ознакомиться с особенностями применения функций в языке Си++, с понятием прототипа и областью его применения, с понятием автоматических внешних, статических и регистровых переменных и их применением при составлении программ с использованием функций. Функции Программы на языке СИ обычно состоят из большого числа отдельных функций (подпрограмм). Как правило, они имеют небольшие размеры и могут находиться как в одном, так и в нескольких файлах. Связь между функциями осуществляется через аргументы, возвращаемые значения и внешние переменные. Вызов функции осуществляется следующим образом: <тип функции >(параметр 1, параметр 2 , …); Если функция имеет переменное число параметров, то вместо последнего из них указывается многоточие. Передача одного значения из вызванной функции в вызвавшую происходит с помощью оператора возврата, который записывается в следующем виде: return (выражение); 56 В этом случае значение выражения (в частном случае может быть просто переменная) передается в основную программу и подставляется вместо обращения к функции. Пусть вызывающая программа обращается к функции следующим образом: a=fun(b,c); Здесь b и c - аргументы, значения которых передаются в вызываемую подпрограмму. Если описание функции начинается так: fun(i,j) , то переменные i и j получат значения a и b соответственно. Пример 1. Оформить получение абсолютной величины числа в виде функции. Сама функция может быть оформлена в виде отдельного файла. В этом случае выполняется его включение процедурой #include. Программа имеет следующий вид: #include <stdio.h> main() {int a=10,b=0,c=-20; int d,e,f; d=abs(a); /*обращение к функции abs*/ b=abs(b); f=abs(c); printf("%d %d %d",d,b,f); } #include "abc.c" /*включение файла abc.c с функцией abs*/ /*Функция, вычисляющая абсолютную величину числа */ abs(x) int x; /*Описание переменных, работающих в функции */ {int y; y=(x<0)?-x:x; /*Определение абсолютной величины числа*/ return (y); /*Возвращает значение у вызывающей программе*/ } В приведенной программе описание типа функции было опущено. Это возможно только в том случае, если возвращенное значение имеет целый тип. Во всех остальных случаях описание типа функции обязательно. приведем пример, когда результатом работы функции будет число двойной точности. Пример 2. Оформить в виде функции вычисление f=x + y/z. В первом примере функция хранилась в виде отдельного файла и включалась процедурой #include. Функция может быть включена в один файл с вызывающей программой. В этом случае процедура #include не требуется, а сама функция должна быть объявлена в основной программе, если она имеет не целый тип. Приведем программу для примера 2, оформленную таким способом. Программа имеет вид: #include <stdio.h> main() { double f,x=5.5,y=10.1,z=20.5, vv() /*объявлены переменные и функция vv*/ f=vv(x,y,z); /*обращение к функции vv*/ printf("lf",f); /*вывод результата */ } /*функция */ double vv(x,y,z) double x,y,z; /*объявление переменных функции */ 57 {double f; f=sqrt(x)+y/z; /*вычисление значения функции */ return(f); /*возврат вычисленного значения функции */ } В языке СИ аргументы функции передаются по значению, т.е. вызванная функция получает временную копию каждого аргумента, а не его адрес. Это означает, что функция не может изменять оригинальный аргумент в вызвавшей ее программе. Однако это легко сделать, если передавать в функцию не переменные, а их адреса. Пример 3. В приведенной ниже программе вводятся некоторые значения переменных а и b, потом в функции izm они меняются местами. #include <stdio.h> main() {int a,b; scanf ("%d %d", &a, &b); izm (&a, &b); /*обращение к функции izm; аргументами являются адреса переменных a и b*/ printf("%d, %d",a, b); /*вывод на экран измененных значений */ } #include "izm.c" /*включение файла izm.c с функцией izm */ /*функция*/ izm(a, d); /*аргументы a и b являются указателями */ int *a, *b; /* *a и *b - значения, на которые указывают указатели */ {int c; c=*a; *a = *b; *b=c; /*обмен местами */ } Функция izm получает копию адресов переменных a и b, меняет местами значения, записанные по этим адресам, и передает управление в основную программу. Адреса &a и &b в основной программе не изменялись, а вот значения, на которые они указывают, поменялись местами. Если в качестве аргумента функции используется имя массива, то ей передается адрес начала массива, а сами элементы не копируются. Функция может изменять элементы массива, сдвигаясь (индексированием) от его начала. Пример 4. В массиве S поменять местами элементы: первый со вторым, третий с четвертым и т.д. Оформить этот алгоритм в виде функции reverse. #include <stdio.h> main() {int i,j,s[6]; /* описание переменных i,j и массива s целого типа */ for (i=0; i<6; i++) scanf("%d",&s[i]); /*ввод элементов массива s*/ reverse(s); /*обращение к функции reverse*/ for (i=0; i<6; i++) printf("%d",s[i]); /*вывод полученного массива */ } include "reverse.c" /*включение файла reverse.c с функцией reverse */ /*функция*/ reverse(s) int s[]; /*описание работающего в подпрограмме массива */ { 58 int a,i; for (i=1; i<5; i+=2) {a=s[i]; s[i]=s[i+1]; s[i+1]=a;} /*обмен элементов местами*/ } Рассмотрим особенности работы функции с двумерным массивом. В предыдущем примере в функции массив был описан как int s[]; для двумерного массива а нельзя записать a[][]. В описании двумерного массива во второй квадратной скобке должно быть указано количество столбцов, например: a[][3]. Пример 5. Увеличить все элементы массива а(5,5) в два раза. Оформить этот алгоритм в виде подпрограммы. #include <stdio.h> main() {int a[5][5]; /*описание массива a*/ int i,j; /*объявление переменных i,j*/ for (i=0;i<5;i++) for (j=0; j<5; j++) scanf("%d",a[i][j]); /*ввод массива*/ mas(a); /*обращение к функции mas*/ for (i=0; i<5; i++) for (j=0; j<5; j++) printf("%d", a[i][j]); /*вывод полученного результата*/ } /*функция*/ mas(a) int a[][5]; /*описание массива а*/ {int i,j; /*описание переменных i,j*/ for (i=0; i<5; i++) for (j=0; j<5; j++) a[i][j] = 2*a[i][j]; /*увеличение элементов массива в 2 раза*/ } Классы памяти В языке СИ различают четыре основных памяти: внешнюю (глобальную), автоматическую (локальную), статическую и регистровую. Внешние переменные определены вне любой из функций, следовательно, доступны для многих из них. Область внешней переменной простирается от точки во входном файле, где она объявлена, и до конца файла. Если внешняя переменная определена в другом файле, то вступает в силу описание extern (внешний). На рис.1 показано, где объявляются и на что распространяется область действия внешних переменных, если программа main и вызываемая функция находятся в данном файле. На рис. 2 демонстрируются отличия, имеющие место, когда main и вызываемая функция находятся в разных файлах. В файле с вызываемой функцией внешние переменные будут доступны после их описания с помощью ключевого слова extern. Пример 5. Оформить в виде функции вычисление выражения: f=ax2+bx+c; В приведенной ниже программе заданные переменные объявлены как внешние, причем основная программа и функция находятся в одном файле. #include <stdio.h> int a=5, b=7, c=10,x; /* Объявление внешних переменных a,b,c,x целого типа*/ main () { int f; scanf ("%d", &x); /*Ввод значения переменной x*/ 59 f=kv(); /*обращение к функции*/ printf ("%d",f); /*вывод на экран значения переменной f*/ } /*функция*/ kv() {int f; f=a*x*x+b*x+c; /*вычисление значения f*/ return (f); /*возвращает значение f вызывающей программе*/ } Если сравнить эту программу с программой, приведенной в примере 2, то можно обнаружить два различия: после имени функции в скобках отсутствуют аргументы; в функции не объявлены переменные, с которыми работает функция. Это стало возможным потому, что переменные объявлены внешними, а значит они известны всему файлу, в том числе и функции. Внешние переменные должны быть описаны до функции main(). Только в этом случае они становятся внешними (см. рис. 1). Приведем программу для этого же примера, рассмотрев случай, когда основная программа и функция расположены в разных файлах. #include <stdio.h> int a=5, b=7, c=10,x,f; /* Объявление внешних переменных a,b,c,x,f целого типа*/ main () { scanf ("%d", &x); /*Ввод значения переменной x*/ f=kv(); /*обращение к функции*/ printf ("%d",f); /*вывод на экран значения переменной f*/ } #include "kv.c" /*включение файла kv.c функцией kv*/ /*функция*/ kv() {extern int a,b,c,x,f; f=a*x*x+b*x+c; /*вычисление значения f*/ return (f); /*возвращает значение f вызывающей программе*/ } Как было сказано выше, если основная программа и функция расположены в разных файлах, то переменные в функции должны быть вписаны при помощи ключевого слова extern. Рассмотрим теперь статические переменные. Статические переменные имеют такую же область действия, как автоматические, но они не исчезают, когда содержащая их функция закончит свою работу. Компилятор хранит их значения от одного вызова функции до другого. Статические переменные объявляются с помощью ключевого слова static. Можно статические переменные описать вне любой функции. Это создает внешнюю статическую переменную. Разница между внешней переменной и внешней статической переменной заключается в области их действия. Обычная внешняя переменная может использоваться функциями в любом файле (с помощью ключевого слова extern), в то время как внешняя статическая переменная может использоваться только функциями того же самого файла. Регистровые переменные относятся к последнему классу. Ключевое слово register указывает, что переменная, о которой идет речь, будет интенсивно использоваться. Если это возможно, значения таких переменных помещаются во внутренние регистры процессора, благодаря чему программа будет более быстрой. 60 1. 2. 3. 4. 5. 6. 7. Задания для самостоятельного выполнения Вычисление факториала числа через рекурсивную функцию. Поиск максимального значения в одномерном массиве через рекурсивную функцию методом половинного деления. Написать программу, вычисляющую разрядность введенного целого числа. Вычисления оформить с помощью рекурсивной функции. Написать программу вычисления значения минимального элемента в массиве. Вычисления оформить с помощью рекурсивной функции. Написать программу, вычисляющую сумму элементов массива. Вычисления оформить с помощью рекурсивной функции. Написать программу, вычисляющую значение n-го члена арифметической прогрессии, заданной соотношением: p( n )=p( n-1 )+2; p( 1 )=1. Написать программу вывода цифр p-ичного представления заданного десятичного натурального числа (p от 2 до 9). Вычисления и вывод оформить в виде рекурсивной функции. Лабораторная работа Тема: Использование библиотечных функций. Рекурсивные определения функций. Передача значений через глобальные переменные Цель: ознакомиться с особенностями применения функций в языке Си++, с понятием прототипа и областью его применения, с понятием автоматических внешних, статических и регистровых переменных и их применением при составлении программ с использованием функций. Библиотечными называются вспомогательные функции, хранящиеся в отдельных файлах. Стандартные библиотеки входят в стандартный комплект системы программирования на Си/Си++. Кроме того, программист может создавать собственные библиотеки функций. Для использования стандартных функций необходимо подключать к программе заголовочные файлы соответствующих библиотек. Делается это с помощью директивы претранслятора #include с указанием имени заголовочного файла. Например, #include <stdio.h>. Все заголовочные файлы имеют расширение h (от английского header). Теперь должно быть понятно, что эти файлы содержат прототипы функций библиотеки. На стадии претрансляции происходит подстановка прототипов перед основной функцией, после чего компилятор в состоянии контролировать правильность обращения к функциям. Сами программы, реализующие функции, хранятся в форме объектного кода и подключаются к основной программе на стадии редактирования связей (при работе компоновщика). Как и в Паскале, в языках Си/Си++ допускается рекурсивное определение функций. Проиллюстрируем определение рекурсивной функции на классическом примере вычисления факториала целого положительного числа. В случае, если при вызове функции будет задан отрицательный аргумент, она вернет нулевое значение — признак неверного обращения. ГЛОБАЛЬНЫЕ И ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ Под глобальные переменные выделяется место во внешней памяти (не нужно думать, что речь идет о магнитной памяти, это оперативная память класса extern). Глобальную переменную можно объявить либо вне программных блоков, либо внутри 61 блока с ключевым словом extern. Обычно это делается в тех случаях, когда программный модуль хранится в отдельном файле и, следовательно, отдельно компилируется. Локальные переменные, объявленные внутри блоков, распределяются в автоматической памяти, работающей по принципу стека. Выделение памяти происходит при входе выполнения программы в блок, а при выходе из блока память освобождается. Ключевое слово auto писать необязательно (подразумевается по умолчанию). Статическая память выделяется под переменные, локализованные внутри блока, но в отличие от автоматической памяти не освобождается при выходе из блока. Таким образом, при повторном вхождении в блок статическая переменная сохраняет свое прежнее значение. Пример объявления статической переменной: f() {static int schet=10; ...} Инициализация статической переменной происходит только при первом вхождении в блок. Если инициализация явно не указана, то переменной автоматически присваивается нулевое начальное значение. Статические переменные можно использовать, например, для организации счетчика числа вхождений в блок. Регистровая память выделяется под локальные переменные. Регистры процессора — самый быстрый и самый маленький вид памяти. Они задействованы при выполнении практически всех операций в программе. Поэтому возможность распоряжаться регистровой памятью лучше оставить за компилятором. Передача значений через переменные Областью действия описания программного объекта называется часть программы, в пределах которой действует (учитывается) это описание. Если переменная описана внутри некоторого блока, то она локализована в этом блоке и из других блоков, внешних по отношению к данному, «не видна». Если описание переменной находится вне блока и предшествует ему в тексте программы, то это описание действует внутри блока и называется глобальным. Глобальная переменная «видна» из блока. Например: double x ; int funcl() (int у;... } void main() {float у;...} Переменная x является глобальной по отношению к функциям funcl, main и, следовательно, может в них использоваться. В функциях funcl и main имеются локальные переменные с одинаковым именем у. Однако это разные величины, никак не связанные друг с другом. Поскольку переменная х является общей для обеих функций, то они могут взаимодействовать через х друг с другом. При обращении к функции передача значений возможна как через параметры, так и через глобальные переменные. Используя этот механизм, в программах на Си можно реализовывать функции, работающие подобно процедурам в Паскале. Задания для самостоятельного выполнения 1. Написать функцию, которая выводит на экран рамку. В качестве параметров должны передаваться координаты верхнего левого угла (оси x, y) и размер (ширина и высота). 2. Составьте функцию, выводящую строку из символов количество, а также вид, которых вводятся с клавиатуры. 3. Написать функцию, специализированную на вывод строки из звездочек, количество которых определяется пользователем. 4. Написать функцию, вычисляющую корни квадратного уравнения. В качестве аргументов она принимает коэффициенты (a, b, c), а возвращает значение по обстоятельству ( x1 и x2, либо «Корней нет», либо а=0 «Введены не корректные данные»). 62 5. Функция возвращает строку (полученную в качестве аргумента), преобразованную из нижнего регистра в верхний. 6. Напишите функцию, которая возвращает 1, если пользователь ввел гласную букву русского алфавита, и 0 в противном случае. 7. Напишете программу, которая выводит таблицу факториалов от 1 до 10, используя для этого функцию. 8. Программа вычисляет процент от числа. Например, 321% от числа 3 равен 9.63. 9. Напишите функцию, которая возводит число a в степень b. Причем a и b вводятся с клавиатуры. 10. Сделайте программу, функция которой сравнивает введенные числа и результат выдает в виде знаков >, < или =. Лабораторная работа Тема: Работа с файлами. Общие сведения. Форматированный ввод –вывод Цель: развитие навыкоа работы с файлами, знать форматы ввода-вывода Работа с дисковым файлом начинается с объявления указателя на поток. Формат такого объявления: FILE *имя указателя; Слово file является стандартным именем структурного типа, объявленного в заголовочном файле stdio.h. В структуре file содержится информация, с помощью которой ведется работа с потоком, в частности: указатель на буфер, указатель (индикатор) текущей позиции в потоке и т.д. Следующий шаг — открытие потока, которое производится с помощью стандартной функции fopen (). Эта функция возвращает конкретное значение для указателя на поток, и поэтому ее значение присваивается объявленному ранее указателю. Соответствующий оператор имеет формат: имя_указателя = {open(имя файла, режим_открытия); Параметры функции fopen() являются строками, которые могут быть как константами, так и указателями на символьные массивы. Например: fp=fopen("test.dat","r"); Здесь test.dat — имя физического файла в текущем каталоге диска, с которым теперь будет связан поток с указателем fp. Параметр режима r означает, что файл открыт для чтения. Что касается терминологии, то допустимо употреблять как выражение «открытие потока», так и выражение «открытие файла». Существуют следующие режимы открытия потока и соответствующие им параметры: Как уже отмечалось при изучении Паскаля, надо хорошо понимать, что открытие уже существующего файла для записи ведет к потере прежней информации в нем. Если такой файл еще не существовал, то он создастся. Открывать для чтения можно только существующий файл. Поток может быть открыт либо для текстового, либо для двоичного (бинарного) режима обмена. 63 При записи в файл осуществляется обратное преобразование. При работе с двоичным файлом никаких преобразований символов не происходит, т.е. информация переносится без всяких изменений. Указанные выше параметры режимов открывают текстовые файлы. Если требуется указать на двоичный файл, то к параметру добавляется буква b. Например: rb, или wb, или r+b. В некоторых компиляторах текстовый режим обмена обозначается буквой t, т.е. записывается a+t или rt. Если при открытии потока по какой-либо причине возникла ошибка, то функция fopen() возвра-щает значение константы NULL. Эта константа также определена в файле stdio.h. Ошибка может возникнуть из-за отсутствия открываемого файла на диске, нехватки места в динамической памяти и т.п. Поэтому желательно контролировать правильность прохождения процедуры открытия файла. В случае ошибки программа завершит выполнение с закрытием всех ранее открытых файлов. Закрытие потока (файла) осуществляет функция fclose (), прототип которой имеет вид: int fclose(FILE *fptr); Здесь fptr обозначает формальное имя указателя на закрываемый поток. Функция возвращает ноль, если операция закрытия прошла успешно. Другая величина означает ошибку. ЗАПИСЬ И ЧТЕНИЕ СИМВОЛОВ Запись символов в поток производится функцией putс() с прототипом int putc(int ch, FILE *fptr); Если операция прошла успешно, то возвращается записанный символ. В случае ошибки возвращается константа EOF. Считывание символа из потока, открытого для чтения, производится функцией gets() с прототипом int gets(FILE *fptr); Функция возвращает значение считываемого из файла символа. Если достигнут конец файла, то возвращается значение EOF. Заметим, что это происходит лишь в результате чтения кода EOF. ЗАПИСЬ И ЧТЕНИЕ ЦЕЛЫХ ЧИСЕЛ Запись целых чисел в поток без преобразования их в символьную форму производится функцией putw () с прототипом int putw(int, FILE *fptr) ; Если операция прошла успешно, то возвращается записанное число. В случае ошибки возвращается константа EOF. Считывание целого числа из потока, открытого для чтения, производится функцией getw() с прототипом int getw(FILE *fptr); Функция возвращает значение считываемого из файла числа. Если прочитан конец файла, то возвращается значение EOF. Под вводом-выводом в программировании понимается процесс обмена информацией между оперативной памятью и внешними устройствами: клавиатурой, дисплеем, магнитными накопителями и т.п. Ввод — это занесение информации с внешних устройств в оперативную память, а вывод — вынос информации из оперативной памяти на внешние устройства. Такие устройства, как дисплей и принтер предназначены только для вывода; клавиатура — устройство ввода. Магнитные накопители (диски, ленты) используются как для ввода, так и для вывода. Основным понятием, связанным с информацией на внешних устройствах ЭВМ, является понятие файла. Всякая операция ввода-вывода трактуется как операция обмена с файлами: ввод — это чтение из файла в оперативную память; вывод — запись информации из оперативной памяти в файл. Поэтому вопрос об организации в языке программирования ввода-вывода сводится к вопросу об организации работы с файлами. 64 Вспомним, что в Паскале мы использовали представления о внутреннем и внешнем файле. Внутренний файл — это переменная файлового типа, являющаяся структурированной величиной. Элементы файловой переменной могут иметь разный тип и, соответственно, разную длину и форму внутреннего представления. Внутренний файл связывается с внешним (физическим) файлом с помощью стандартной процедуры Assign. Один элемент файловой переменной становится отдельной записью во внешнем файле и может быть прочитан или записан с помощью одной команды. Попытка записать в файл или прочитать из него величину, не совпадающую по типу с типом элементов файла, приводит к ошибке. Аналогом понятия внутреннего файла в языках Си/Си++ является понятие потока. Отличие от файловой переменной Паскаля состоит в том, что потоку в Си не ставится в соответствие тип. Поток — это байтовая последовательность, передаваемая в процессе ввода-вывода. Поток должен быть связан с каким-либо внешним устройством или файлом на диске. В терминологии Си это звучит так: поток должен быть направлен на какое-то устройство или файл. Основные отличия файлов в Си состоят в следующем: здесь отсутствует понятие типа файла и, следовательно, фиксированной структуры записи файла. Любой файл рассматривается как байтовая последовательность. Признаком конца файла является стандартная константа EOF. Существуют стандартные потоки и потоки, объявляемые в программе. Последние обычно связываются с файлами на диске, создаваемыми программистом. Стандартные потоки назначаются и открываются системой автоматически. С началом работы любой программы открываются 5 стандартных потоков, из которых основными являются следующие: - stdin — поток стандартного ввода (обычно связан с клавиатурой); - stdout — поток стандартного вывода (обычно связан с дисплеем); - stderr — вывод сообщений об ошибках (связан с дисплеем). Кроме этого, открывается поток для стандартной печати и дополнительный поток для последовательного порта. ФОРМАТИРОВАННЫЙ ВВОД-ВЫВОД В ЯЗЫКЕ СИ++ Для организации ввода-вывода в Си++ можно использовать средства языка Си (conio.h). Однако в Си++ существует стандартная библиотека классов, ориентированная на организацию потокового ввода-вывода. Классы ввода-вывода образуют иерархию по принципу наследования. Базовым в этой иерархии является класс ios (исключение составляют лишь классы буферизи-рованных потоков). В классе ios объединены базовые данные и методы для ввода-вывода. Прямыми потомками класса ios являются классы istream и ostream. Класс istream — это класс входных потоков; ostream — класс выходных потоков. Потомком этих двух классов является iostream — класс двунаправленных потоков ввода-вывода. С этим классом мы уже много раз имели дело, подключая его к программам с помощью головного файла iostream.h. Объект cout принадлежит к классу ostream и представляет собой поток вывода, связанный с дисплеем. Объект cin принадлежит классу istream и является потоком ввода, связанным с клавиатурой. Оба эти объекта наследуются классом iostream. Знак << обозначает перегруженную операцию вставки символов в поток вывода cout, а >> — знак операции извлечения из потока ввода cin. Для организации форматированного потокового ввода-вывода в Си++ существуют два средства: - применение функций-членов класса ios для управления флагами форматирования; - применение функций-манипуляторов. Задания для самостоятельного выполнения 65 1. Заполнить файл некоторым количеством целых случайных чисел. 2. Найти сумму и количество целых чисел, записанных в бинарный файл. 3. Поместить в файл n записей, содержащих сведения о кроликах, содержащихся в хозяйстве: пол (m/f), возраст (в мес.), масса. 4. В бинарном файле хранятся сведения о кроликах, содержащихся в хозяйстве: пол (m/f), возраст (в мес.), масса. Найти наиболее старого кролика. Если таких несколько, то вывести информацию о том из них, масса которого больше. 5. Заполнить файл значениями функции y = x * cos x. 6. Файл содержит несколько строк, в каждой из которых записано единственное выражение вида a#b (без ошибок), где a, b - целочисленные величины, # - операция +, -, /, *. Вывести каждое из выражений и их значения. В заданном файле целых чисел посчитать количество компонент, кратных 3. 4. САМОСТОЯТЕЛЬНАЯ РАБОТА СТУДЕНТА СРСП 1 Тема: Системное программирование. Основные понятие и определения. Цель: знать основные понятия и определения Программы и программное обеспечение Программа — это данные, предназначенные для управления конкретными компонентами системы обработки информации (СОИ) в целях реализации определенного алгоритма. Обратить внимание: программа — это данные. Один из основных принципов машины фон Неймана — то, что и программы, и данные хранятся в одной и той же памяти. Сохраняемая в памяти программа представляет собой некоторые коды, которые могут рассматриваться как данные. Возможно, с точки зрения программиста программа — активный компонент, она выполняет некоторые действия. Но с точки зрения процессора команды программы — это данные, которые процессор читает и интерпретирует. С другой стороны программа — это данные с точки зрения обслуживающих программ, например, с точки зрения компилятора, который на входе получает одни данные — программу на языке высокого уровня (ЯВУ), а на выходе выдает другие данные — программу в машинных кодах. Программное обеспечение (ПО) — совокупность программ СОИ и программных документов, необходимых для их эксплуатации Существенно, что ПО — это программы, предназначенные для многократного использования и применения разными пользователями. В связи с этим следует обратить внимание на ряд необходимых свойств ПО. Необходимость документирования По определению программы становятся ПО только при наличии документации. Конечный пользователь не может работать, не имея документации. Документация делает возможным тиражирование ПО и продажу его без его разработчика. По Бруксу ошибкой в ПО является ситуация, когда программное изделие функционирует не в соответствии со своим описанием, следовательно, ошибка в документации также является ошибкой в программном изделии. Эффективность ПО, рассчитанное на многократное использование (например, ОС, текстовый редактор) пишется и отлаживается один раз, а выполняется многократно. Таким образом, выгодно переносить затраты на этап производства ПО и освобождать от затрат этап выполнения, чтобы избежать тиражирования затрат. Надежность В том числе: Тестирование программы при всех допустимых спецификациях входных данных 66 Защита от неправильных действий пользователя Защита от взлома — пользователи должны иметь возможность взаимодействия с ПО только через легальные интерфейсы. Появление ошибок любого уровня не должно приводить к краху системы. Ошибки должны вылавливаться диагностироваться и (если их невозможно исправить) превращаться в корректные отказы. Системные структуры данных должны сохраняться безусловно. Сохранение целостности пользовательских данных желательно. Возможность сопровождения Возможные цели сопровождения — адаптация ПО к конкретным условиям применения, устранение ошибок, модификация. Во всех случаях требуется тщательное структурирование ПО и носителем информации о структуре ПО должна быть программная документация. Адаптация во многих случаях может быть передоверена пользователю — при тщательной отработке и описании сценариев инсталляции и настройки. Исправление ошибок требует развитой сервисной службы, собирающей информацию об ошибках и формирующей исправляющие пакеты. Модификация предполагает изменение спецификаций на ПО. При этом, как правило, должны поддерживаться и старые спецификации. Эволюционное развитие ПО экономит вложения пользователей. Системное программирование Системная программа — программа, предназначенная для поддержания работоспособности СОИ или повышения эффективности ее использования. Прикладная программа — программа, предназначенная для решения задачи или класса задач в определенной области применения СОИ. В соответствии с терминологией, системное программирование — это процесс разработки системных программ (в том числе, управляющих и обслуживающих). С другой стороны, система — единое целое, состоящее из множества компонентов и множества связей между ними. Тогда системное программирование — это разработка программ сложной структуры. Эти два определения не противоречат друг другу, так как разработка программ сложной структуры ведется именно для обеспечения работоспособности или повышения эффективности СОИ. Подразделение ПО на системное и прикладное является до некоторой степени устаревшим. Сегодняшнее деление предусматривает по меньшей мере три градации ПО: Системное Промежуточное Прикладное Промежуточное ПО (middleware) мы определяем как совокупность программ, осуществляющих управление вторичными (конструируемыми самим ПО) ресурсами, ориентированными на решение определенного (широкого) класса задач. К такому ПО относятся менеджеры транзакций, серверы БД, серверы коммуникаций и другие программные серверы. С точки зрения инструментальных средств разработки промежуточное ПО ближе к прикладному, так как не работает на прямую с первичными ресурсами, а использует для этого сервисы, предоставляемые системным ПО. С точки зрения алгоритмов и технологий разработки промежуточное ПО ближе к системному, так как всегда является сложным программным изделием многократного и многоцелевого использования и в нем применяются те же или сходные алгоритмы, что и в системном ПО. Современные тенденции развития ПО состоит в снижении объема как системного, так и прикладного программирования. Основная часть работы программистов выполняется в промежуточном ПО. Снижение объема системного программирования 67 определено современными концепциями ОС, объектно-ориентированной архитектурой и архитектурой микроядра, в соответствии с которыми большая часть функций системы выносится в утилиты, которые можно отнести и к промежуточному ПО. Снижение объема прикладного программирования обусловлено тем, что современные продукты промежуточного ПО предлагают все больший набор инструментальных средств и шаблонов для решения задач своего класса. Значительная часть системного и практически все прикладное ПО пишется на языках высокого уровня, что обеспечивает сокращение расходов на их разработку/модификацию и переносимость. Системное ПО подразделяется на системные управляющие программы и системные обслуживающие программы. Управляющая программа — системная программа, реализующая набор функций управления, который включает в себя управление ресурсами и взаимодействие с внешней средой СОИ, восстановление работы системы после проявления неисправностей в технических средствах. Программа обслуживания (утилита) — программа, предназначенная для оказания услуг общего характера пользователям и обслуживающему персоналу СОИ. Управляющая программа совместно с набором необходимых для эксплуатации системы утилит составляют операционную систему (ОС). Кроме входящих в состав ОС утилит могут существовать и другие утилиты (того же или стороннего производителя), выполняющие дополнительное (опционное) обслуживание. Как правило, это утилиты, обеспечивающие разработку программного обеспечения для операционной системы. Система программирования — система, образуемая языком программирования, компилятором или интерпретатором программ, представленных на этом языке, соответствующей документацией, а также вспомогательными средствами для подготовки программ к форме, пригодной для выполнения. СРСП 2 Тема: Арифметические операции и арифметические выражения Цель: развитие навыков записи арифметических операции и выражений на языке С++ Выполнение вычислений составляют основу подавляющего большинства компьютерных программ. Для этой цели C++ использует множество арифметических операций, каждая из которых имеет свой знак. Ниже приведена таблица наиболее употребительных бинарных операций, то есть операций c двумя аргументами, с примерами их программной записи название операции знак в С/C++ алгебраическое выражение запись на С/C++ сложение + f+7 f+7 вычитание p-c p-c умножение * bm b*m деление / x/y x/y остаток от деления % r mod s r% s Круглые скобки в арифметических выражениях C++ используются так же, как в алгебре. При определении порядка вычислений сложных выражений C++ придерживается стандартных правил старшинства операций: первыми выполняются операции в скобках; затем операции умножения, деления, и вычисления остатка; в завершение - операции сложения и вычитания. Пример: алгебра: запись на С/C++: . В последнем выражении скобки необходимы для сохранения правильного порядка операций - сначала суммирование, затем деление. 68 Кроме арифметических операций, язык C++ дает удобные возможности использования математических функций. Большая их часть содержится в библиотеке math.h, и для пользования ими требуется соответствующая директива #include <math.h>. Наиболее употребительные математические функции приведены в таблице: название функции обозначение запись в С/С++ синус sin(x) косинус cos(x) тангенс tan(x) квадратный корень sqrt(x) возведение в степень pow (x, y) экспонента exp(x) натуральный логарифм log(x) модуль fabs(x) арксинус asin(x) арккосинус acos(x) арктангенс atan(x) Все перечисленные функции принимают в качестве аргумента вещественную переменную (или константу) и возвращают вещественный результат. Использовать функции можно путем их вызова, аналогично вызову функций ввода-вывода. Например, в результате выполнения следующего набора операторов переменная y получит значение квадратного корня из 2: float x = 2.0; float y = sqrt(x); СРСП 3 Тема: Автоматическое преобразование типов и операции приведения. Цель: знать общие правила записи выражений. Операции ( ) и [ ]. В языке Си круглые и квадратные скобки рассматриваются как операции, причем эти операции имеют наивысший приоритет. Их смысл будет раскрыт позже. Подведем итог всему разговору об операциях Си/Си++, сведя их в общую табл. 4.2 и расположив по рангам. Ранг операции — это порядковый номер в ряду приоритетов. Чем больше ранг, тем ниже приоритет. В таблице отражено еще одно свойство операций — ассоциативность. Если одна и та же операция, повторяющаяся в выражении несколько раз, выполняется в порядке расположения слева направо, то она называется левоассоциативной; если выполняется справа налево, то операция правоассоциативная. В таблице эти свойства отображены стрелками влево и вправо. Некоторые операции, присутствующие в таблице, пока не обсуждались. Таблица 4.2 Приоритеты (ранги) операций 69 Приведение типов при вычислении выражений. Практически во всех языках программирования высокого уровня работает ряд общих правил записи выражений: • все символы, составляющие выражение, записываются в строку (нет надстрочных и подстрочных символов); • в выражении проставляются все знаки операций; • при записи выражения учитываются приоритеты операций; • для влияния на последовательность операций используются круглые скобки. Некоторые специфические особенности записи выражений на Си были описаны выше при рассмотрении операций языка. В процессе вычисления выражений с разнотипными операндами производится автоматическое преобразование типов величин. Знание программистом правил, по которым происходят эти преобразования, предупреждает некоторые ошибки в записи выражений. Суть правил преобразования при выполнении бинарных операций сводится к следующему: • преобразование не выполняется, если оба операнда имеют одинаковый тип; • при разных типах операндов происходит приведение величины с младшим типом к старшему типу (кроме операции присваивания); • при выполнении операции присваивания величина, полученная в правой части, преобразуется к типу переменной, стоящей слева от знака =. Старшинство типов друг по отношению к другу определяется по следующему принципу; старший тип включает в себя все значения младшего типа как подмножество. Вещественные (плавающие) типы являются старшими по отношению к целым. В свою очередь, внутри каждой из этих групп иерархия типов устанавливается в соответствии с указанным принципом. Целые типы по возрастанию старшинства расположены в таком порядке: char→shot→int→long Порядок старшинства вещественных типов следующий: float→double→long double Следует иметь в виду, что при преобразовании целой величины к плавающему типу может произойти потеря точности (вместо 1 получится 0,999). 70 Следующий пример иллюстрирует порядок выполнения операций и происходящие преобразования типов при вычислении выражения ( цифры вверху — порядок операций). char ch; int i; float f; double d; long double r; Упражнения 1. Определить тип константы: а) 315; б)-32.4; в) 102408; г) 3.7Е57; д) 0315; е) 0х24; ж) 2.6L; з) 70700U; и) ' 5'; к) '\121'. 2. В программе объявлена переменная: int n=10. Определить результаты вычислений следующих выражений: а) n++; б)++n; в) n%2; г) n/3; д) n/3.; е) ++n+5; ж) 5+n++; з) (float)n/4; и) sizeof(n); к) sizeof(l.*n). 3. Координаты точки на плоскости заданы переменными Х и Y. Записать следующие условия в форме логических выражений: а) точка лежит в первой четверти координатной плоскости; б) точка лежит на оси X; в) точка лежит на одной из осей; г) точка лежит в первой или второй четверти внутри единичной окружности; д) точка лежит на единичной окружности в третьей или четвертой четверти; е) точка лежит внутри кольца с внутренним радиусом 1 и внешним радиусом 2 во второй или четвертой четверти. 4. В программе объявлена переменная: float х=2. Какое значение получит переменная х в результате вычисления следующих выражений? а) х+=2; б) х/=10; в) х*=(х+1); г) х+=х+=х+=1. 5. Определить значения выражений для трех вариантов объявления переменной х: 1) float x=l.; 2) float x=10.; 3) int x=l. a) x>i?2*x:x; б) x/5==2?5:x/10; в) х>0&&х<=1?1:0. СРСП 4 Тема: Ввод и вывод в языке Си. Общие концепции. Цель: знать ввода и вывода в языке 71 Поскольку текст программ на С и на С++ часто путают, то путают иногда и потоковый ввод-вывод С++ и функции ввода-вывода семейства printf для языка С. Далее, т.к. С-функции можно вызывать из программы на С++, то многие предпочитают использовать более знакомые функции ввода-вывода С. По этой причине здесь будет дана основа функций ввода-вывода С. Обычно операции ввода-вывода на С и на С++ могут идти по очереди на уровне строк. Перемешивание их на уровне посимвольного ввода-вывода возможно для некоторых реализаций, но такая программа может быть непереносимой. Некоторые реализации потоковой библиотеки С++ при допущении ввода-вывода на С требуют вызова статической функции-члена ios::sync_with_stdio(). В общем, потоковые функции вывода имеют перед стандартной функцией С printf() то преимущество, что потоковые функции обладают определенной типовой надежностью и единообразно определяют вывод объектов предопределенного и пользовательского типов. Основная функция вывода С есть int printf(const char* format, ...) и она выводит произвольную последовательность параметров в формате, задаваемом строкой форматирования format. Строка форматирования состоит из объектов двух типов: простые символы, которые просто копируются в выходной поток, и спецификации преобразований, каждая из которых преобразует и печатает очередной параметр.Каждая спецификация преобразования начинается с символа %, например printf("there were %d members present.",no_of_members); Здесь %d указывает, что no_of_members следует считать целым и печатать как соответствующую последовательность десятичных цифр. Если no_of_members==127, то будет напечатано there were 127 members present. Набор спецификаций преобразований достаточно большой и обеспечивает большую гибкость печати. За символом % может следовать: - необязательный знак минус, задающий выравнивание влево в указанном поле для преобразованного значения; d необязательная строка цифр,задающая ширину поля; если в преобразованном значении меньше символов, чем ширина строки, то оно дополнится до ширины поля пробелами слева (или справа, если дана спецификация выравнивания влево); если строка ширины поля начинается с нуля, то дополнение будет проводится нулями, а не пробелами; . необязательный символ точка служит для отделения ширины поля от последующей строки цифр; d необязательная строка цифр, задающая точность, которая определяет число цифр после десятичной точки для значений в спецификациях e или f, или же задает максимальное число печатаемых символов строки; * для задания ширины поля или точности может использоваться * вместо строки цифр. В этом случае должен быть параметр целого типа, который содержит значение ширины поля или точности; h необязательный символ h указывает, что последующая спецификация d, o, x или u относится к параметру типа короткое целое; l необязательный символ l указывает, что последующая спецификация d, o, x или u относится к параметру типа длинное целое; % обозначает, что нужно напечатать сам символ %; параметр не нужен; c символ, указывающий тип требуемого преобразования. Символы преобразования и их смысл следующие: d Целый параметр выдается в десятичной записи; 72 o Целый параметр выдается в восьмеричной записи; x Целый параметр выдается в шестнадцатеричной записи; f Вещественный или с двойной точностью параметр выдается в десятичной записивида [-]ddd.ddd, где число цифр после точки равно спецификации точности для параметра. Если точность не задана, печатается шесть цифр; если явно задана точность 0, точка и цифры после нее не печатаются; e Вещественный или с двойной точностью параметр выдается в десятичной записи вида [-]d.ddde+dd; здесь одна цифра перед точкой, а число цифр после точки равно спецификации точности для параметра; если она не задана печатается шесть цифр; g Вещественный или с двойной точностью параметр печатается по той спецификации d, f или e, которая дает большую точность при меньшей ширине поля; c Символьный параметр печатается. Нулевые символы игнорируются; s Параметр считается строкой (символьный указатель), и печатаются символы из строки до нулевого символа или до достижения числа символов, равного спецификации точности; но, если точность равна 0 или не указана, печатаются все символы до нулевого; p Параметр считается указателем и его вид на печати зависит от реализации; u Беззнаковый целый параметр печатается в десятичной записи. Несуществующее поле или поле с шириной, меньшей реальной, приведет к усечению поля. Дополнение пробелами происходит, если только спецификация ширины поля больше реальной ширины. Ниже приведен более сложный пример: char* src_file_name; int line; char* line_format = "\n#line %d \"%s\"\n"; main() { line = 13; src_file_name = "C++/main.c"; printf("int a;\n"); printf(line_format,line,src_file_name); printf("int b;\n"); } в котором печатается int a; #line 13 "C++/main.c" int b; Использование printf() ненадежно в том смысле, что нет никакого контроля типов. Так, ниже приведен известный способ получения неожиданного результата - печати мусорного значения или чего похуже: char x; // ... printf("bad input char: %s",x); Однако, эти функции обеспечивают большую гибкость и знакомы программирующим на С. Как обычно, getchar() позволяет знакомым способом читать символы из входного потока: int i;: while ((i=getchar())!=EOF) { // символьный ввод C // используем i } 73 Обратите внимание: чтобы было законным сравнение с величиной EOF типа int при проверке на конец файла, результат getchar() надо помещать в переменную типа int, а не char. СРСП 5 Тема: Стандартные функции для работы с файлами и каталогами. Цель: иметь представление о стандартной библиотеке С Для работы с файлами и каталогами в стандартной библиотеке С предусмотрена билиотека dir.h Из-за того, что на всех платформах API, отвечающее за эти функции различно, горазда удобнее использовать эти функции, решающие проблему переносимости. Сделаю сразу несколько оговорок. Так как С - мультиплатформенный язык, а UNIX/LINUX не поддерживает систему наименования дисков Windows, то за 0 принимают текущий диск, за 1 диск, соответствущий в Windows/DOS путю 'A:', 2 - 'B:' и т. д. Если вы работаете в среде DOS, Вы можете заметить, что некоторые функции переопределены с префиксом _dos. Для их использования необходимо подключить заголовочный файл dos.h Для некоторых функций может потребоваться stdio.h #include <io.h> #include <dir.h> Управление именами файлов Для того, чтобы сформировать путь или поделить его на строки, содержащие устройство (диск), каталог, имя файла и расширение, необходимо использовать соответсвенно функции fnmege (File Name MERGE) и fnsplit (File Name SPLIT) Давайте раcсмотрим их прототипы: void fnmerge(char *path, const char *drive, const char *dir, const char *name, const char *ext); void _wfnmerge(wchar_t *path, const wchar_t *drive, const wchar_t *dir, const wchar_t *name, const wchar_t *ext ); int fnsplit(const char *path, char *drive, char *dir, char *name, char *ext); int _wfnsplit(const wchar_t *path, wchar_t *drive, wchar_t *dir, wchar_t *name, wchar_t *ext ); Длина строк, используемых в этих функциях ограничена. По стандартам ANSI C она равна: Константа 16 бит 32 бита Описание MAXPATH 80 256 Путь MAXDRIVE 3 3 Устройство, включая двоеточие. Каталог, включая начальные и MAXDIR 66 260 конечные обратные слеши (\) MAXFILE 9 256 Имя, без расширения MAXEXT 5 256 Расширение, включает точку Таким образом, чтобы использовать эти функции, необходимо объявить массивы строк длиной в MAX* символов: char path[MAXPATH]; strcpy(path, "C:\\WinNT\\explorer.exe"); /*path содержит C:\WinNT\explorer.exe*/ char drive[MAXDRIVE]; char dir[MAXDIR]; char file[MAXFILE]; 74 char ext[MAXEXT]; fnsplit(path,drive,dir,file,ext); /* Переменная Значение * drive c: * dir \WinNT\ * file explorer * ext .exe */ file[0]=0; /*Очистка имени файла и разширения*/ ext[0]=0; /*Теперь в path строка C:\WinNT\*/ fnmerge(path,drive,dir,file,ext); Для переименования или переименования файла на диске применяют функцию rename. int rename(const char *oldname, const char *newname); int _wrename(const wchar_t *oldname, const wchar_t *newname); Эта функция перемещает/переименовывает файл из пути oldname в newname и возращает -1 или нуль, если операция проведена успешно. Я настоятельно рекомендую Вам пользоваться ограничениями MAX* во избежание недоразумений По причине того, что в С обратный слеш (\) используется для внутренних целей, при объявлении пути слеш удваивают. Создание и удаление файлов и каталогов Для того, чтобы создать каталог достаточно вызова функции mkdir. Ее преимущество в том, что если в нее передать дополнительный параметр, она доступна в UNIX-системах. Единственный ее параметр - путь к каталогу. int mkdir(const char *path); int _wmkdir(const wchar_t *path); Как и функция rename, при ошибке она возвращает -1, а при успехе - 0. Для того, чтобы удалить созданный раннее каталог достаточно вызова rmdir. К каталогу, который удаляем предъявляются три требования: - он должен буть пустым - он не является текущей рабочей директорией - он не является корневым каталогом int _rmdir(const char *path); int _wrmdir(const wchar_t *path); Для того, чтобы создать файл на диске, используйте функцию _creat, которая создает новый или обрезает существующий файл. Она, как и многие функции io.h работает с файлами с помощью "хэндлов", и поэтому после нее следует вызвать функцию close. Тип файла (текстовый или двоичный) задается глобальной переменной _fmode (O_TEXT или O_BINARY соответственно). Если вы не хотите обрезать старый файл, воспользуйтесь функцией creatnew. int creatnew(const char *path, int mode /*или int attrib*/); При ошибке возвращает -1, при успехе нуль. Первый параметр указывает на путь к файлу, второй на требуемые аттрибуты: Атрибут Объяснение FA_RDONLY Только для чтения FA_HIDDEN Скрытый 75 Системный FA_SYSTEM int _creat(const char *path, int amode); int _wcreat(const wchar_t *path, int amode); Первый параметр - путь к создаваемогу файлу, второй - режим файла. Файл пожет быть открыт и для записи, и для чтения, и для обеих операций. Значение режима Доступ S_IWRITE Разрешение на запись S_IREAD Разрешение на чтение S_IREAD|S_IWRITE Разрешение на запись и чтение Эти константы храняться в библиотеке sys/stat.h Для того, чтобы настроить разрешения для каталога (аналогично режиму файла), используйте функцию chmod. int chmod(const char *path, int amode); int _wchmod(const wchar_t *path, int amode); Функция устанавливает режим amode для каталога path. Для того, чтобы узнать режим файла, используйте функцию access. int access(const char *filename, int amode); int _waccess(const wchar_t *filename, int amode); Определяет возможен ли доступ к файлу filename при режиме amode. Значение Объяснение 6 Возможно ли чтение и запись 4 Возможно ли чтение 2 Возможна ли запись 1 Выполнение (игнорируется) 0 Проверить только на наличие Функция access возвращает значение 0, если файл имеет заданный режим mode. Возвращаемое значение -1 свидетельствует о том, что названный файл не существует или недоступен в заданном amode. Существует два способа удалить файл: через функцию _unlink или remove. int remove(const char *filename); int _wremove(const wchar_t *filename); Функция удаляет файл filename и возвращает нуль при успехе. Ее аналог - функция _unlink еще не утверждена стандартом ANSI C. int _unlink(const char *filename); int _wunlink(const wchar_t *filename); Замечание: если файл открыт, следует закрыть его перед удалением. Так как ФС помимо имени файла хранит еще много различных параметров, я приведу несколько методов получения их. Первый - использование поиска файлов по маске. Для не обязательно иметь конкретное имя файла. Поиск файлов по маске Для того, чтобы создать свой файловый броузер программисты используют метод, называемый поиском файлов по маске. Он заключается в том, что в текущей папке находятся все файлы, удовлетворяющие данной маске. При каждом новом вызове 76 функций программа получает данные о следующем файле. Как правило, для реализации этого метода требуется три функции: - Инициализация поиска. Поиск первого файла. - Поиск n-нного файла (в цикле) - Завершение поиска. Для операционной системы MSDOS этими функциями соответственно являются _dos_findfirst, _dos_findnext. Заключительная функция отсутствует, поиск завершает _dos_findnext. Если Вы примените этот метод в Win32, Вам потребуются функции WinAPI FindFirstFile, FindNextFile, FindClose. Для UNIX - систем следует воспользоваться библиотекой dirent.h и функциями opendir, readdir, closedir и rewinddir. Нже будет рассмотрен пример для DOS. Для того, чтобы начать поиск следует указать маску поиска, параметры поиска и место в памяти, куда будут помещаться выходные данные. struct find_t { char reserved[21]; /* резерв */ char attrib; /* атрибуты файла */ int wr_time; /* время создания */ int wr_date; /* дата создания */ long size; /* размер файла */ char name[13]; /* имя 8.3 */ }; unsigned findfirst(const char *pathname, int attrib, struct find_t *ffblk); /* В некоторых компиляторах - _dos_findfirst */ Маской является приблизительное наименование файла, например: Маска Объяснение *.* Все файлы с расширением * Все файлы *.txt Все файлы txt ????? Файлы с именем из пяти букв *.? Все файлы с расширением из одной буквы a* Все файлы, начинающиеся с буквы 'a' Все файлы с именем из трех букв, первая из a?? которых 'a' В качестве параметра attrib передаются параметры (атрибуты) файла. Они могут объединяться: FA_HIDDEN | FA_RDONLY. Следует обратиться к справке вашего компилятора, чтобы узнать полный список названий атрибутов. Атрибут Альтернативное название Объяснение FA_NORMAL _A_NORMAL Нормальный FA_RDONLY _A_RDONLY Только для чтения FA_HIDDEN _A_HIDDEN Скрытый FA_SYSTEM _A_SYSTEM Системный FA_LABEL _A_VOLID Метка тома FA_DIREC _A_SUBDIR Папка FA_ARCH _A_ARCH Архивный Когда найден первый файл, следует вызвать функцию findnext. 77 /* В некоторых компиляторах - _dos_findnext */ unsigned findnext(struct find_t *ffblk); Вот пример поиска файлов в текущей папке (MSDOS/BC5). find_t files[128]; /*До 128 файлов*/ struct find_t ffblk; int maxfiles = 0; int done; /*Поиск файлов и папок*/ done = _dos_findfirst("*.*",_A_NORMAL|_A_SUBDIR,&ffblk); while (!done) { maxfiles++; if(maxfiles==120) break; memcpy(&files[maxfiles],&ffblk, sizeof(find_t)); done = _dos_findnext(&ffblk); } Функции stat, fstat Второй метод - файловая статистика. В С она реализована с помощью функций fstat (stat) int fstat(int handle, struct stat *statbuf); int stat(const char *path, struct stat *statbuf); int _wstat(const wchar_t *path, struct stat *statbuf); Главое отличие fstat от stat заключается в том, что для нее имя файла не обязательно, а достаточно дескриптора (про дескрипторы мы говорили во вотрой части статьи). Второй параметр - указатель на структуру, хранящую файловую статистику. Эта структура принимает такой вид: struct stat { short st_dev; //Дескриптор устройства или номер файла на диске short st_ino; short st_mode; //Режим дескриптора short st_nlink; int st_uid; int st_gid; short st_rdev; long st_size; //Размер в байтах //Файловое время: long st_atime; //для NTFS/HPFS - время последнего доступа к файлу long st_mtime; //для NTFS/HPFS - время последней модификации файла long st_ctime; //для NTFS/HPFS - время создания файла }; Для того, чтобы проверить режим дескриптора также объявлены макросы: S_ISDIR(m) - проверяет на директорию S_ISCHR(m) - проверяет на устройство S_ISBLK(m) - проверяет на специальный тип блока S_ISREG(m) - проверяет на файл S_ISFIFO(m) - проверяет на специальный режим FIFO Проверка каждого атрибута по-отдельности Для некоторых атрибутов проверку можно провести специальными функциями, например filelength возвращает длину файла handle, getftime позволяет получить дату и время создания файла. 78 Методические рекомендации по СРС СРС 1 Тема: История. Создание. Развитие и стандартизация языка. История названия. Цель: Расскрыть ЯП Язык возник в начале 1980-х годов, когда сотрудник фирмы Bell Laboratories Бьёрн Страуструп придумал ряд усовершенствований к языку C под собственные нужды. До начала официальной стандартизации язык развивался в основном силами Страуструпа в ответ на запросы программистского сообщества. В 1998 году был ратифицирован международный стандарт языка C++: ISO/IEC 14882:1998 «Standard for the C++ Programming Language»; после принятия технических исправлений к стандарту в 2003 году — нынешняя версия этого стандарта — ISO/IEC 14882:2003. Ранние версии языка, известные под именем «C с классами», начали появляться с 1980 года. Идея создания нового языка берёт начало от опыта программирования Страуструпа для диссертации. Он обнаружил, что язык моделирования Simula имеет такие возможности, которые были бы очень полезны для разработки большого программного обеспечения, но работает слишком медленно. В то же время язык BCPL достаточно быстр, но слишком близок к языкам низкого уровня и не подходит для разработки большого программного обеспечения. Страуструп начал работать в Bell Labs над задачами теории очередей (в приложении к моделированию телефонных вызовов). Попытки применения существующих в то время языков моделирования оказались неэффективными. Вспоминая опыт своей диссертации, Страуструп решил дополнить язык C (преемник BCPL) возможностями, имеющимися в языке Симула. Язык C, будучи базовым языком системы UNIX, на которой работали компьютеры Bell, является быстрым, многофункциональным и переносимым. Страуструп добавил к нему возможность работы с классами и объектами. В результате, практические задачи моделирования оказались доступными для решения как с точки зрения времени разработки (благодаря использованию Симула-подобных классов) так и с точки зрения времени вычислений (благодаря быстродействию C). В начале в C были добавлены классы (с инкапсуляцией), производные классы, строгая проверка типов, inline-функции и аргументы по умолчанию. Разрабатывая C с классами (позднее C++), Страуструп также написал программу cfront — транслятор, перерабатывающий исходный код C с классами в исходный код простого C. Новый язык, неожиданно для автора, приобрёл большую популярность среди коллег и вскоре Страуструп уже не мог лично поддерживать его, отвечая на тысячи вопросов. В 1983 году произошло переименование языка из C с классами в C++. Кроме того, в него были добавлены новые возможности, такие как виртуальные функции, перегрузка функций и операторов, ссылки, константы, пользовательский контроль над управлением свободной памятью, улучшенная проверка типов и новый стиль комментариев (//). Его первый коммерческий выпуск состоялся в октябре 1985 года. В 1985 году вышло также первое издание «Языка программирования C++», обеспечивающее первое описание этого языка, что было чрезвычайно важно из-за отсутствия официального стандарта. В 1989 году состоялся выход C++ версии 2.0. Его новые возможности включали множественное наследование, абстрактные классы, статические функции-члены, функции-константы и защищённые члены. В 1990 году вышло «Комментированное справочное руководство по C++», положенное впоследствии в основу стандарта. Последние обновления включали шаблоны, исключения, пространства имён, новые способы приведения типов и булевский тип. Стандартная библиотека C++ также развивалась вместе с ним. Первым добавлением к стандартной библиотеке C++ стали потоки ввода/вывода, обеспечивающие 79 средства для замены традиционных функций C printf и scanf. Позднее самым значительным развитием стандартной библиотеки стало включение в неё Стандартной библиотеки шаблонов. Никто не обладает правами на язык C++, он является свободным. Однако сам документ стандарта языка (за исключением черновиков) не доступен бесплатно. Стандартизация языка В 1998 году был опубликован стандарт ISO/IEC 14882:1998 (известный как C++98), разработанный комитетом по стандартизации C++ (ISO/IEC JTC1/SC22/WG21 working group). В течение нескольких лет после опубликования документа, рассмотрев сообщения об ошибках, комитет разработал исправленную версию стандарта, ISO/IEC 14882:2003, вышедшую в 2003 году. В 2005 году был выпущен отчёт «Library Technical Report 1» (кратко называемый TR1). Не являясь официально частью стандарта, отчёт описывает расширения стандартной библиотеки, которые, как ожидалось авторами, должны быть включены в следующую версию языка C++. Степень поддержки TR1 улучшается почти во всех поддерживаемых компиляторах языка C++. В настоящее время разрабатывается новая версия стандарта, неофициально называемая C++0x. История названия Название C++ было придумано Риком Масситти (Rick Mascitti) и впервые было использовано в декабре 1983 года. Ранее, на этапе разработки, новый язык назывался «C с классами». Имя языка, получившееся в итоге, происходит от оператора унарного постфиксного инкремента C ++ (увеличение значения переменной на единицу). Имя C+ не было использовано потому, что является синтаксической ошибкой в C и, кроме того, это имя было занято другим языком. Язык также не был назван D, поскольку «является расширением C и не пытается устранять проблемы путём удаления элементов C». СРС 2 Тема: Философия C++. Общие принципы. Совместимость с языком С. Цель: Знать принципы. Некоторые принципы: Получить универсальный язык со статическими типами данных, эффективностью и переносимостью языка C. Непосредственно и всесторонне поддерживать множество стилей программирования, в том числе процедурное программирование, абстракцию данных, объектно-ориентированное программирование и обобщённое программирование. Дать программисту свободу выбора, даже если это даст ему возможность выбирать неправильно. Максимально сохранить совместимость с C, тем самым делая возможным лёгкий переход от программирования на C. Избежать разночтений между C и C++: любая конструкция, допустимая в обоих языках, должна в каждом из них обозначать одно и то же и приводить к одному и тому же поведению программы. Избегать особенностей, которые зависят от платформы или не являются универсальными. «Не платить за то, что не используется» — никакое языковое средство не должно приводить к снижению производительности программ, не использующих его. Не требовать слишком усложнённой среды программирования. Совместимость с языком С Выбор именно C в качестве базы для создания нового языка программирования объясняется тем, что язык C: 80 1. является многоцелевым, лаконичным и относительно низкоуровневым языком; 2. подходит для решения большинства системных задач; 3. исполняется везде и на всём; 4. стыкуется со средой программирования UNIX. Несмотря на ряд известных недостатков языка C, Страуструп пошёл на его использование в качестве основы, так как «в C есть свои проблемы, но их имел бы и разработанный с нуля язык, а проблемы C нам известны». Кроме того, это позволило быстро получить прототип компилятора (cfront), который лишь выполнял трансляцию добавленных синтаксических элементов в оригинальный язык C. По мере разработки C++ в него были включены другие средства, которые перекрывали возможности конструкций C, в связи с чем неоднократно поднимался вопрос об отказе от совместимости языков путём удаления устаревших конструкций. Тем не менее, совместимость была сохранена из следующих соображений: сохранение действующего кода, написанного изначально на C и прямо перенесённого в C++; исключение необходимости переучивания программистов, ранее изучавших C (им требуется только изучить новые средства C++); исключение путаницы между языками при их совместном использовании («если два языка используются совместно, их различия должны быть или минимальными, или настолько большими, чтобы языки было невозможно перепутать»). СРС 3 Тема: Необъектно-ориентированные возможности. Комментарии. Типы. Цель: отличие объектно-ориентированного подхода от не объектно-ориентированного В настоящем объектно-ориентированном языке все элементы так называемой предметной области (problem domain) выражаются через концепцию объектов.Ообъекты — это центральная идея объектно-ориентированного программирования. Многие из нас, обдумывая какую-то проблему, вряд ли оперируют понятиями "структура", "пакет данных", "вызов функций" и "указатели", ведь привычнее применять понятие "объекты". Возьмем такой пример. Допустим, вы создаете приложение для выписки счета-фактуры, в котором нужно подсчитать сумму по всем позициям. Какая из двух формулировок понятней с точки зрения пользователя? Не объектно-ориентированный подход Заголовок счета-фактуры представляет структуру данных, к которой я получу доступ. В эту структуру войдет также дважды связанный список структур, содержащих описание и стоимость каждой позиции. Поэтому для получения общего итога по счету мне потребуется объявить переменную с именем наподобие totallnvoiceAmount и инициализировать ее нулем, получить указатель на головную структуру счета, получить указатель на начало связанного списка, а затем "пробежать" по всему этому списку. Просматривая структуру для каждой позиции, я буду брать оттуда переменную-член, где находится итог для данной позиции, и прибавлять его к totallnvoiceAmount. Объектно-ориентированный подход У меня будет объект "счет-фактура", и ему я отправлю сообщение с запросом на получение общей суммы. Мне не важно, как информация хранится внутри объекта, как это было в предыдущем случае. Я общаюсь с объектом естественным образом, запрашивая у него информацию посредством сообщений. (Группа сообщений, которую объект в состоянии обработать, называется интерфейсом объекта. Чуть ниже я объясню, почему в объектно-ориентированном подходе вместо термина "реализация" правильней употреблять термин "интерфейс".) 81 Очевидно, что объектно-ориентированный подход естественнее и ближе к тому способу рассуждений, которым многие из нас руководствуются при решении задач. Во втором варианте объект "счет-фактура", наверно, просматривает в цикле совокупность (collection) объектов, представляющих данные по каждой позиции, посылая им запросы на получение суммы по данной позиции. Но если требуется получить только общий итог, то вам все равно, как это реализовано, так как одним из основных принципов объектноориентированного программирования является инкапсуляция (encapsulation). Инкапсуляция — это свойство объекта скрывать свои внутренние данные и методы, представляя наружу только интерфейс, через который осуществляется программный доступ к самым важным элементам объекта. Как объект выполняет задачу, не имеет значения, главное, чтобы он справлялся со своей работой. Имея в своем распоряжении интерфейс объекта, вы заставляете объект выполнять нужную вам работу. (Ниже я остановлюсь на понятиях "инкапсуляция" и "интерфейс".) Здесь важно отметить, что разработка и написание программ моделирования реальных объектов предметной области облегчается тем, что представить поведение таких объектов довольно просто. Заметьте: во втором подходе от объекта требовалось, чтобы он произвел нужную вам работу, т. е. подсчитал общий итог. В отличие от структуры, в объект по определению входят не только данные, но и методы их обработки. Это значит, что при работе с некоторой проблемной областью можно не только создать нужные структуры данных, но и решить, какие методы связать с данным объектом, чтобы объект стал полностью инкапсулированной частью функциональности системы. СРС 4-5 Тема: Объектно-ориентированные особенности языка. Описание функций в теле класса. Константные функции-члены. Наследование. Полиморфизм. Инкапсуляция. Цель: знать осебенно языка. С++ имеет механизмы для объектно ориентированного программирования и потому поддерживает объектно ориентированный подход. Однако, личное дело программиста: воспользоваться или нет преимуществами этой философии. Можно писать на языке С++ программы, как и прежде в С, не пользуясь объектно ориентированной добавкой С++. Ведь С-программирование - это часть С++. Потому можно писать в С++ и очень успешно С-программы, не пользуясь объектно ориентированной идеологией С++. Но тогда в каком же случае и при каких обстоятельствах можно, нужно, выгодно и имеет смысл использовать концепцию объектной ориентированности? Давайте во всем этом разберемся не спеша. Чтобы хорошо понимать, в чем же тут дело, и где, как говорят, "собака зарыта"? Как я уже имел возможность упоминать, первоначально программирование для компьютеров было преимущественно связано с решением математических задач. Программист сам работал за пультом ЭВМ: он писал на машинном языке свои программы и затем сам же вводил команды в память компьютера и отлаживал их без использования специальных средств автоматизации. Естественно, этот процесс был очень трудоемким и медленным. Элементарный выход был найден в разделении труда. На Вычислительном центре стали повсеместно вводить должность оператора, которому поручалась работа по вводу информации в память ЭВМ. Теперь программист мог большую часть своего рабочего времени проводить за свои рабочим столом: думать и программировать, но он должен был еще и готовить задание оператору. Это, конечно, несколько повысило эффективность его труда. Но не столь резко. Программист все же предпочитал сам выходить со своей задачей для отладки на компьютер. 82 В то время аппаратные средства и, в частности, память ЭВМ, были еще весьма ограничены. Компьютерные сбои были очень частым и весьма распространенным явлением. Но по мере увеличения мощности оборудования и снижения его себестоимости значительно расширялся круг задач. Росла сложность задач, решаемых на компьютере. Возникала настоятельная потребность в ускорении процесса программирования без потери качества программ. Значительно усложнялось и само программитрование. Возникли, как я уже упомянул, и первые проблемы в нем. Для их преодоления были изобретены подпрограммы, библиотеки подпрограмм, а затем и первые языки программирования: - Fortran - для решения инженерных задач; - Cobol - для решения экономических задач; - Pascal - для обучения методам структурного программирования. Были предложены новые методики в программировании, например, структурное программирование. Это были выдающиеся попытки осмыслить проблемы организации труда программиста и Вычислительного центра в целом. Они дали свои положительные результаты. Были в то время и первые попытки создавать специальные языки программирования для начинающих. Например, таким был всем известный Basic. В момент его изобретения авторы языка должны были ограничиться только заглавными буквами латинского алфавита, так как реальная память не позволяла им еще использовать и строчные буквы. Но технический прогресс в области вычислительной техники развивается такими семимильными шагами, так стремительно и быстро, что вскоре были изобретены новые запоминающие устройства для оперативной памяти компьютера, и память стала такой дешевой, что поменялась идеология программирования. Теперь уже никто не настаивал и не говорил об экономии ячеек памяти. Это перестало быть актуальным. Памяти теперь было, что называется, навалом. Можно было разрабатывать гигантские комплексы программ, состоящие не просто из 50000 команд, а превышающие этот рубеж в 2-3 и более раз. Но возможности памяти человека оставались прежними. Становилось все труднее, следуя процедурному программитрованию, отслеживать поведение функций, из которых строилась, как правило, программа. На смену процедурному подходу к программированию пришел объектно ориентированный подход. Были введены понятия объектов, классов и другие нововведения, которые и делают С++ объектно ориентированным. Таким образом, объектно ориентированное программирование позволило успешно программировать огромные, точнее сверх большие комплексы программ, и именно в этих случаях его применение оправдано. 2. Процедурное и объектно ориентированное программирование. Отличительным признаком процедурного программирования является использование подпрограмм, процедур и функций. Программисты давно поняли, что задача решается проще, если ее разложить на подзадачи. Эти подзадачи и стали основой разработки подпрограмм. В языке Паскаль подпрограммы - это процедуры и функции. В языке Си роль подпрограмм выполняют функции. В объектно ориентированном программировании идут дальше подпрограмм, процедур и функций. Степень обобщения реального мира такова, что, как в биологии и естествознвании, в программировании в последнее время вводятся объекты и из них образуются классы объектов, совершенно аналогичные объектам и классам объектов в других разделах знаний. Например, рассмотрим задачу пополнения вклада на сберегательном счету в банке. При процедурном подходе первое, что приходит на ум - это дробить поставленную задачу 83 на более мелкие самостоятельные части - подзадачи. В данном случае можно предложить в задаче "Внести вклад" следующие подзадачи: сохранение первого вклада; рассчёт нового баланса: 1. поиск предыдущего баланса; 2. добавление новой суммы вклада; 3. запоминание нового баланса.<>/LI> Этот подход наглядно изображен ниже на следующем рисунке: На этом рисунке Вы видите "дерево" подпрограмм. Но с увеличением сложности задачи количество "ветвей" на "дереве" возрастает. И если решаемая задача слишком большая, то и количество подзадач становится огромным и трудноуправляемым. В таком случае даже группе программистов будет весьма сложно осмыслить всю задачу в целом и следить за ее развитием и реализацией отдельными программистами. Естественно, что указать ту границу, за которой сложность процедурного подхода становится решающим фактором, сдерживающим моментом программирования, довольно трудно. Некоторые специалисты оценивают эту "точку фазового перехода" примерно в 100000 строк кода программы. Так из процедурного подхода возник новый объектно ориентированный подход в программировании. В объектно ориентированном программировании программист уходит от такого деления задачи на мелкие части. Он пытается увидеть задачу как некую "абстракцию" реального мира. Его интересуют взаимодействия между этими идеализированными "абстракциями" - объектами реального мира. Таким образом, он должен, используя свой опыт и знания, увидеть эти "абстракции", распознать их в своей задаче. СРС 6 Тема: Отличия от языка C. Новые возможности. Дальнейшее развитие. Общие направления развития C++. Цель: знать отличия, раскрыть перспективы. Нововведениями C++ в сравнении с C являются: >>>поддержка объектно-ориентированного программирования; >>>поддержка обобщённого программирования через шаблоны; >>>дополнительные типы данных; >>>исключения; >>>пространства имён; >>>встраиваемые функции; >>>перегрузка операторов; >>>перегрузка имён функций; >>>ссылки и операторы управления свободно распределяемой памятью; >>>дополнения к стандартной библиотеке. 84 Язык C++ во многом является надмножеством C. Новые возможности C++ включают объявления в виде выражений, преобразования типов в виде функций, операторы new и delete, тип bool, ссылки, расширенное понятие константности, подставляемые функции, аргументы по умолчанию, переопределения, пространства имён, классы (включая и все связанные с классами возможности, такие как наследование, функции-члены, виртуальные функции, абстрактные классы и конструкторы), переопределения операторов, шаблоны, оператор ::, обработку исключений, динамическую идентификацию и многое другое. Язык C++ также во многих случаях строже относится к проверке типов, чем C. В C++ появились комментарии в виде двойной косой черты (//), которые были в предшественнике C — языке BCPL. Некоторые особенности C++ позднее были перенесены в C, например, ключевые слова const и inline, объявления в циклах for и комментарии в стиле C++ (//). В более поздних реализациях C также были представлены возможности, которых нет в C++, например макросы vararg и улучшенная работа с массивами-параметрами. C++ не включает в себя C Несмотря на то, что большая часть кода C будет справедлива и для C++, C++ не является надмножеством C и не включает его в себя. Существует и такой верный для C код, который неверен для C++. Это отличает его от Objective C, ещё одного усовершенствования C для ООП, как раз являющегося надмножеством C. Существуют и другие различия. Например, C++ не разрешает вызывать функцию main() внутри программы, в то время как в C это действие правомерно. Кроме того, C++ более строг в некоторых вопросах; например, он не допускает неявное приведение типов между несвязанными типами указателей и не разрешает использовать функции, которые ещё не объявлены. Более того, код, верный для обоих языков, может давать разные результаты в зависимости от того, компилятором какого языка он оттранслирован. Например, на большинстве платформ следующая программа печатает «С», если компилируется компилятором C, и «C++» — если компилятором C++. Так происходит из-за того, что символьные константы в C (например, 'a') имеют тип int, а в C++ — тип char, а размеры этих типов обычно различаются. #include <stdio.h> int main() { printf("%s\n", (sizeof('a') == sizeof(char)) ? "C++" : "C"); return 0; } Современное состояние языка Приведем беглое описание новых возможностей языка, введенных с момента публикации ARM и до публикации проекта стандарта в апреле 1995г. 1. Ведены новые ключевые слова-синонимы для операций (and, and_eq, bitand, bitor, compl, not, or, or_eq, xor, xor-equ). 2. В языке появился булевский тип данных bool и литералы этого типа true и false. 3. Появился механизм пространств имен (namespace), облегчающий совместную реализацию проектов группами программистов и позволяющий избегать проблем при использовании нескольких библиотек (ключевые слова namespace и using). 4. Новое ключевое слово explicit позволяет запретить нежелательное использование конструкторов как функций преобразования типов. 5. Изменены синтаксис и семантика для изменения прав доступа к членам классов. Новый механизм позволяет использовать единый синтаксис для использования членов пространств имен и членов классов. При этом несколько изменились правила 85 выбора наиболее подходящей из набора совместно используемых функций (на основе использования ключевого слова using). 6. Добавлен механизм явного использования rtti (включающий операцию с ключевым словом typeid и класс type_info стандартной библиотеки). 7. Добавлены новые и скорректированы старые способы явного преобразования типов (static_cast, dynamic_cast, const_cast и reinterpret_cast). 8. Добавлена новая операция new[], парная к операции delete[]; для операций new и delete изменена семантика выражения размещения с целью более безопасной обработки исключительных ситуаций в конструкторах. Стандартная операция new теперь не может вернуть значение 0 в случае нехватки памяти или ошибки, а генерирует исключительную ситуацию. Старый вариант, возвращающий 0, доступен программисту только c явным указанием. 9. Объявления переменных теперь возможны не только в заголовке for-цикла, но и в операторах if, while, do-while, switch. 10. Более точно определено время жизни временных объектов в выражении. Теперь время их жизни ограниченно полным выражением, а не концом текущего блока, как сказано в ARM. 11. Полностью переработано определение шаблонов в Си++. Теперь уже нельзя сказать, что шаблоны Си++ являются лишь слегка замаскированными синтаксическими подстановками. Для них обязателен синтаксический разбор и контроль семантики (в максимально возможной степени). Неоднозначности внутри тел шаблонов, вызываемые неизвестной природой типовых параметров, явно разрешаются посредством ключевого словом typename. 12. Допускаются шаблонные функции-члены нешаблонных классов, вложенные шаблонные классы и шаблоны - параметры шаблонов. 13. Виртуальные функции могут возвращать тип, отличный от типа подменяемой функции базового класса, если эти типы являются указателями или ссылками на производный и базовый класс. 14. Перечислимый тип (enum) окончательно утвердился как самостоятельный тип, не являющийся ни одним из целочисленных типов. Теперь разрешено совместное использование функций, основанное на этом различии; константа 0 перечислимого типа более не считается целочисленным 0, запрещено ее неявное преобразование к указательному типу. 15. Ослаблено ограничение на тип, возвращаемый операцией ->. Теперь это может быть практически произвольный тип. 16. Добавлено (на редкость бессмысленное) ключевое слово mutable, позволяющее допускать изменение членов объекта константного класса. Более подробно некоторые из этих изменений рассмотрены в статье [6]. Перспективы Очень хочется, чтобы стандарт языка был наконец принят. Это может подстегнуть разработчиков систем программирования для Си++ на максимально полную поддержку новых возможностей языка, которые в настоящий момент не реализованы. Хочется, чтобы язык был наконец зафиксирован, и в него не добавлялись новые средства, противоречащие старой идеологии. И, наконец, несколько замечаний по поводу продолжительных дискуссий на темы вроде "Что лучше - Си или Си++" или "Является ли Си++ языком ООП". Си++ изначально не был "академическим" языком программирования. Его часто подвергали и подвергают критике за неклассический подход к реализации поддержки ООП, даже за то, что его непосредственным предшественником был язык Си, который явно не подходил на роль "академического" языка, но очень популярен в среде профессиональных разработчиков. Бесполезно критиковать Си++ за недостаточно неполную или "неправильную" поддержку 86 ООП. Си++ вполне укладывается в формулировку парадигмы ООП, данную Страуструпом. Си++ не является единственным языком ООП и имеет право на свои недостатки и своеобразие. Сейчас ООП явно поддерживается в нескольких языках программирования, во многих не так как Си++, и разработчик имеет возможность выбора (так, превосходно спроектированный объектно- ориентированный язык Ada95 уже стандартизован). Постоянные упреки в адрес Си++ за "неправильное" понимание ООП кажутся абсолютно несуразными. Даже такие столпы ООП, как Грэди Буч [9] и Роберт Мартин [10] признают, что Си++ является вполне подходящим и неплохим инструментом ООП. Благодаря своим корням Си++ был быстро воспринят разработчиками, уже имеющими опыт программирования на Си, но не избалованными дорогими и малоэффективными языками "чистого" объектно-ориентированного программирования. Противопоставлять Си и Си++ тоже не стоит. Один из них ортогонально расширяет другой по нескольким направлениям, которые могут быть использованы независимо. Си++ допускает использование в нескольких вариантах. Во-первых, он содержит язык программирования Си, точнее, его стандартизованный диалект ANSI C почти целиком. При этом Си++ обладает более мощными и строгими правилами проверки типов. Программист волен использовать лишь часть возможностей Си++, не сталкиваясь при этом с неприятностями, связанными с неполным знанием языка (то есть воспринимать Си++ как "улучшенный" Си). Можно использовать только механизм классов и наследования, не пользуясь возможностями обобщенного (generic) программирования, основанного на шаблонах. Наконец, многим разработчикам приглянулась возможность обработки исключительных ситуаций, не зависящая от других механизмов языка. Взятые вместе, эти возможности предлагают разработчику современный, достаточно гибкий и мощный язык программирования высокого уровня с поддержкой ООП. 87 Примерные вопросы: 1. Основные понятия. язык Borland C++. Элементы языка Borland C++. 2. Структура простой программы. Функции ввода-вывода. Форматы преобразования данных. 3. Понятие пустого и составного операторов. Операторы перехода 4. Операторы цикла 5. Массивы 6. Указатели 7. Функции 8. Классы памяти. Рекурсивные функции 9. Командная строка. Параметры функции main() 10. Структуры 11. Размещение полей битов в структурах 12. Объединения 13. Динамическое использование памяти. Библиотечные функции 14. Файлы 15. Графические возможности языка Borland C++ 16. Структура простейшей программы. Построение блок-схем линейных процессов 17. Использование условных операторов. Форматированный вывод на экран. Форматированный ввод с клавиатуры 18. Программирование циклических алгоритмов. 19. Цикл с предусловиемЦикл с постусловием. 20. Программирование циклических алгоритмов. Цикл с параметром. 21. Массивы и строки. 22. Многомерные массивы. 23. Работа с символьными строками. 24. Функции. Формат определения функции. 25. Вызов функции. Прототипы функций 26. Использование библиотечных функци. Рекурсивные определения функций. Передача значений через глобальные переменные 27. Работа с файлами. Общие сведения. Форматированный ввод –вывод. . 88