Инфоурок Информатика Другие методич. материалыКурс лекций "Программная инженерия".

Курс лекций "Программная инженерия".

Скачать материал

МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ

РОССИЙСКОЙ ФЕДЕРАЦИИ

Старооскольский технологический институт

им. А.А. УГАРОВА  

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

«Национальный исследовательский технологический университет «МИСиС» 

 

 

Кафедра АИСУ

 

 

 

Михайлюк Е.А.

 

 

 

 

Программная инженерия

Часть 1

 

 

 

 

 

 

Учебно-методическое пособие

(курс лекций)

для студентов специальности

080801 – «Прикладная информатика»

(очная, очно-заочная, заочная формы обучения)

 

 

 

 

Одобрено редакционно-издательским советом института

 

 

 

 

 

 

Старый Оскол

2014


УДК  004

ББК  32.973.26-018

 

 

 

 

 

 

 

Рецензент: Сизов С.В. – к.ф.-м.н., руководитель подразделения разработки ПО ООО «МДЦ-Консалтинг»

 

 

 

 

 

Михайлюк Е.А. Программная инженерия (Часть 1). Учебно-методическое пособие. Старый Оскол: СТИ НИТУ «МИСиС», 2014. – 168 с.

 

 

 

 

 

 

 

Учебно-методическое пособие для студентов специальности 080801 – «Прикладная информатика», для всех форм обучения.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

© Михайлюк Е.А.

© СТИ НИТУ «МИСиС»


Содержание

 

Введение.. 5

Глава I Правила оформления программ на Си/Си++. 6

1.1 Алгоритмы.. 6

1.2. Сравнение языков С++ и С.. 10

1.3. Структура программы на языке С++. 11

Глава 2. Основы программирования на С++. 16

2.1. Данные в языке С++. 16

2.2. Переменные языка С++. 18

2.3. Константы в языке С++. 21

2.4. Операции в языке С++. 22

2.5. Логические операции и операции отношения. 26

2.6. Стандартные математические функции в языке С++. 27

2.7. Ввод-вывод в языке Си. 28

2.8.Указатели и массивы.. 31

2.9.Операторы.. 35

Глава3 Использование библиотечных процедур СИ.. 38

3.1.О библиотеке СИ.. 38

3.2.Вызов библиотек СИ.. 39

3.2.Идентификация функций и макро. 39

3.3 Include-файлы.. 41

3.4 Объявление функций. 41

3.5 Проверка типов аргументов. 42

3.6 Обработка ошибок. 43

3.7 Имена файлов и path-имена. 43

Глава 4 Циклические алгоритмы, их реализация в С++. 44

4.1. Циклические алгоритмы.. 44

4.2. Оператор цикла for. 45

4.3. Оператор while. 46

4.4. Оператор do-while. 47

4.5. Операторы break, continue. 47

4.6. Решение задач с использованием циклов. 48

Глава 5. Использование функций при программировании                                 на С/С++ функциях.. 57

5.1. Структура программ.. 57

5.2. Передача параметров в С/С++. 58

5.3.Решения практических задач. 59

5.4. Рекурсивные функции в С/С++. 63

5.5. Область видимости переменных в функциях С/С++. 66

5.6. Перегрузка и шаблоны функций. 69

5.7. Использование значений формальных параметров по умолчанию.. 72

Глава 6. Обработка массивов в языке Си++. 73

6.1. Описание  массивов. 73

6.2. Ввод элементов массива. 74

6.3. Вывод элементов массива. 75

6.4. Основные алгоритмы обработки массивов. 75

Глава 7 Указатели, динамические массивы... 83

7.1. Указатели в С++. 83

7.2. Операции * и & при работе с указателями. 84

7.4. Арифметические операции над адресами. 86

7.6. Примеры программ.. 90

Глава 8. Матрицы. Многомерные массивы... 97

8.1.Блок-схемы основных алгоритмов обработки матриц. 98

8.2. Свойства матриц. 98

8.3.Динамические матрицы.. 101

Глава 9 Объектно-ориентированное программирование (ООП). 112

Классы в С++. 112

9.1 История развития. 113

9.2.Главные понятия и разновидности. 113

9.3. Определение ООП и его основные концепции. 115

9.4. Подходы к проектированию программ в целом.. 119

9.5 Критика ООП.. 122

9.6.Объектно-ориентированные языки. 124

Глава 10. Основные виды, этапы проектирования и жизненный цикл программных продуктов.. 154

Список литературы... 166

 

 

 

 

 


Введение

Си является языком функций, типов данных, операторов присваивания и управления последовательностью вычислений. Большинство функций возвращают некоторые значения. Значение, возвращаемое функцией, может использоваться в операторе присваивания, который изменяет значение другой переменной. Си - язык высокого уровня, способствующий хорошему стилю программирования. Си имеет небольшой набор типов данных: целые числа, числа с плавающей запятой, битовые поля и перечислимый тип. Адресная арифметика языка Си является чувствительной к типу данных того объекта, с которым связан используемый указатель. Разрешены также указатели к функциям. Можно расширить список типов данных путем создания структур с иерархической зависимостью входящих в него типов данных. Каждый тип данных может принадлежать либо к основному типу, либо к ранее описанному структурному типу. Объединения напоминают структуры, но определяют различные виды иерархических зависимостей, в которых данные разных типов располагаются в памяти. Функции Си являются рекурсивными по умолчанию.

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

 


Глава 1. Правила оформления программ на Си/Си++

 

1.1 Алгоритмы

Основные термины:

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

2.                 Программа - это алгоритм, записанный на языке программирования.

3.                 Языком программирования называется специальный язык, понятный для компьютера.

4.                 Программирование - это процесс создания, отладки и тестирования программ.

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

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

1.                 Дискретность - алгоритм должен быть представлен как последовательное выполнение простых шагов.
Шагом называется каждое действие алгоритма.

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

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

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

Способы записи алгоритма

1.                 Формальный - запись алгоритма словесно, на естественном языке.

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

В блок-схеме действия алгоритма (блоки) изображаются следующими геометрическими фигурами:

Действия алгоритма

 

Виды алгоритмов

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

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

Линейный алгоритм

Блоки алгоритма 1, 2, 3 выполняются именно в такой последовательности, после чего алгоритм достигает цели и заканчивается.

2.                 Алгоритм с ветвлением - в зависимости от выполнения или невыполнения условия, исполняется либо одна, либо другая ветвь алгоритма.

Алгоритм с ветвлениями

В данном алгоритме проверяется условие, и если оно выполняется, то есть на вопрос можно ответить "Да", исполняется блок алгоритма 1 (одно ли несколько действий), а если не выполняется - ответ на вопрос отрицательный, то исполняется блок 2.

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

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

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

Алгоритм выполняется так: выполняется блок 1, проверяется условие, если оно не выполняется, то блок 1 выполняется снова и условие проверяется заново. При выполнении условия алгоритм заканчивается.

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

 

Этапы создания программы

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

2.                 Разработка интерфейса (интерфейс - способ общения) - создание экранной формы (окна программы).

3.                 Составление алгоритма.

4.                 Программирование - создание программного кода на языке программирования.

5.                 Отладка программы - устранение ошибок.

6.                 Тестирование программы - проверка правильности ее работы.

7.                 Создание документации, помощи

 

1.2. Сравнение языков С++ и С

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

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

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

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

Выбор С в качестве базового языка для С++ объясняется следующими его достоинствами:

(1) универсальность, краткость и относительно низкий уровень;

(2) адекватность большинству задач системного программирования;

(3) он идет в любой системе и на любой машине;

(4) полностью подходит для программной среды UNIX.

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

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

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

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

(3) существуют сотни тысяч программистов, знающих С; им достаточно овладеть только новыми средствами С++ и не надо изучать основ языка;

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

Язык С сам развивался в последние несколько лет, что отчасти было связано с разработкой С++ [14]. Для изучения С++ не обязательно знать С. Программирование на С способствует усвоению приемов и даже трюков, которые при программировании на С++ становятся просто ненужными. Например, явное преобразование типа (приведение) , в С++ нужно гораздо реже, чем в С. Тем не менее, хорошие программы на языке С по сути являются программами на С++. Например, все программы из классического описания С являются программами на С++. В процессе изучения С++ будет полезен опыт работы с любым языком со статическими типами

 

 

1.3. Структура программы на языке С++

Перед тем как, процессор выполнить какое либо действие, он должен получить инструкцию для его выполнения в виде машинных кодов. Набор таких инструкций называется программой. Авторы первых программ писали их в машинном (двоичном) коде. Теперь программы пишутся на каком либо языке высокого уровня (Pascal, Delphi, C, C++, C++ Builder, Visual C++). Затем специальная программа, называемая транслятором, переводит их машинный код, который и исполняется процессором. Все трансляторы делятся на два класса:

·                     интерпретаторы - трансляторы, которые переводят каждый оператор программы в машинный код и по мере перевода операторы выполняются процессором;

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

Особенности компиляции (перевода в машинный код) программ на языке С++ рассмотрим после написания первой программы.

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

Объявление глобальных переменных

Тип_ результата main(Список_переменных)

{Операторы

}Тип_результата f1(Список_переменных)

{Операторы }

Тип_результата f2(Список_переменных)

{Операторы }…

Тип_результата fn(Список_переменных)

{Операторы}

Здесь Тип_результата - тип возвращаемого функцией значения.

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

int main()

{

Операторы

}

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

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

Символы // означают, что оставшаяся часть строки является комментарием. Текст, начинающийся /* и заканчивающийся */, также является комментарием. Только некоторые компиляторы допускают вложенные комментарии.

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

-строчные и прописные буквы английского алфавита;

-цифры от 0 до 9 (имя не может начинаться с цифры);

-символ подчеркивания.

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

#include <iostream.h> // этот файл нужен для ввода/вывода

void main(void) // главная функция main(void) без параметров

{ // начало функции

char *name; // так описывается строка

cout<<“Введите свое имя”<<endl; // на экране появляется просьба ввести имя

cin>>name>>endl; // с клавиатуры вводится имя

cout<<“Привет, “<<name<<“ это первая программа”; // вывод на экран

} // конец функции

 

В программе можно использовать константы:

десятичные (например: 1, 10L, -6, -2.3, 3.45е6, 6.789е-2);

восьмеричные (например: 012, 0204, 076663L);

шестнадцатеричные (например: 0xa, 0xA, 0xdeL, 0x84, 0x7dB3);

символы (например: ‘g’, ‘?’, ‘\b’, ‘\\’, ‘0x1B’, ‘\’’);

строчные литералы (“long string”, “Y\\N”, “ \”Yes, I do, \” she said”).

 


Пример простейшей программы на языке С++

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

Задача № 1.1. Известны длины двух катетов прямоугольного треугольника. Найти длину гипотенузы, площадь прямоугольника и величины его углов.

#include <stdio.h>

#include <math.h>

 int main()

{

float a,b,c, alf,bet,s;

printf("A=");

scanf("%f",&a);

printf("B=");

scanf("%f",&b);

s=a*b/2;

c=pow(a*a+b*b,0.5);

alf=atan(a/b);

bet=3.14159/2-alf;

printf("\n A=%5.2f \t B=%5.2f \t C=%5.2f \n",a,b,c);

printf("\nS=%5.2f \t alf=%3.0f \t bet=%3.0f\n", s,alf*180/3.14159,bet*180/3.14159);

}

Листинг 1.1.

Рассмотрим подробно текст программы.

Строки 1-2. Указывают компилятору (а точнее препроцессору), что надо использовать функции из библиотек, stdio.h и math.h. В данном случае они необходимы для выполнения операторов ввода scanf и вывода printf (stdio.h) и математических функций возведения в степень pow и вычисление арктангенса atan (math.h).

Строка 5. Описание вещественных переменных a,b,c,alf,bet,s.

Строка 6. Оператор вывода строки символов А=.

Строка 7. Оператор ввода вещественного числа a. В этом операторе (а точнее в функции scanf) должны быть два параметра:

·                     строка, в которой указывают типы вводимых переменных, перед кодом типа должен быть символ %; символ f используется для указания вещественного типа, d – для целого типа;

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

Строка 8. Оператор вывода строки символов B=.

Строка 9. Оператор ввода вещественного числа b.

Строка 10. Оператор присваивания для вычисления площади треугольника. В операторе присваивания могут использоваться знаки операций: +, -, *, /.

Строка 11. Оператор присваивания для вычисления гипотенузы с использованием теоремы Пифагора. Функция pow(x,y) используется в Си для вычисления xy.

Строка 12-13. Операторы присваивания для вычисления углов α и β.

Строки 14-15. Функции вывода результатов на экран. В функции вывода printf должны быть два параметра:

·                     строка вывода, в которой вместо выводимых переменных указываются их типы, между символом % и кодом формата можно указывать формат вывода, который в случае вывода вещественных чисел имеет вид m.n (m - количество позиций в числе, n – количество позиций в дробной части числа); в строке вывода могут использоваться можно поставить знаки: \n перевод строки, \t – табуляция.

·                     список выводимых переменных.

Строка комментариев в Си начинается символами //.

Мы рассмотрели простейшую программу на языке С++, состоящую из операторов ввода данных, операторов присваивания (в которых происходит расчет по формулам) и операторов вывода. Рассмотренные в первой программе операторы ввода-вывода являются операторами классического C, в дальнейшем мы рассмотрим и операторы С++.

Основные этапы обработки программы на языке С++ и формирования машинного кода.

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

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

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

 

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

1.                 Какова структура программы на Си?

2.                  Какие константы используются в Си?

3.                 Что такое алгоритм?

4.                 Перечислите основные свойства алгоритма.

5.                 Укажите основные алгоритмические конструкции.

6.                 Что называется программой?

7.                 В чем отличие компилятора от интерперетатора?

8.                 Для чего служат индентификаторы?

9.                 Укажите основные этапы обработки программы на языке С++ и формирования машинного кода.

10.            Перечислите основные достоинства языка С/С++.

 

Глава 2. Основы программирования на С++

2.1. Данные в языке С++

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

Язык С++ ведет свое начало с языка С и включает в себя все типы данных С.  В языке С определены пять основных типов данных: char – символьные, int – целые, float – с плавающей точкой, double – двойной точности, void – без значения (бестиповый). На базе этих типов формируются другие. Данные типа char всегда занимают один байт. Размер и диапазон представления остальных типов определяется конкретной системой программирования, операционной системой и процессором. Стандарт языка С (С89), принятый в декабре 1989 года, определяет минимальный диапазон значений. В 1999 году стандарт языка Си был расширен до стандарта C99: добавлены некоторые числовые библиотеки, ряд узкоспециальных средств.

Следует учитывать, что вещественные числа хранятся в экспоненциальной форме mE±p, где m – мантисса (целое или дробное число с десятичной точкой), p – порядок (целое число). Для того, чтобы перевести число в экспоненциальной форме к обычному представлению с фиксированной точкой, необходимо мантиссу умножить на десять в степени порядок.

Например,
-6.42Е+2 = -6.42.102 = -642
-3.2E-6 = -3.2.10-6 =-0.0000032

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


 

Таблица 2.1. Типы данных языка С++

Типы данных

Размер в байтах

Минимально допустимый диапазон значений

Комментарий

Целочисленные значения

char  

   1   

-128 .. 127  

 

unsigned char

1

0 .. 255

 

signed char

1

-128 .. 127

 

int

2 или 4

-32768 .. 32767

 

unsigned int

2 или 4

0 .. 65535

 

signed int

2 или 4

-32768 .. 32767  

 

short int

2

-32768 .. 32767  

 

unsigned short int

2

0 .. 65535

 

signed short int

2

-32768 .. 32767

 

long int

4

-2147483648 ..  2147483647

 

long long int

8

-(263-1) .. (263-1)

добавлен стандартом C99

signed long int

4

-2147483648 ..  2147483647

 

unsigned long int

4

0 .. 4294967295

 

unsigned long long int

8

0 .. 264-1

добавлен стандартом C99

Вещественные значения

float

4

 

точность мантиссы не менее 6 знача-щих цифр (типичным является диа-пазон 3.4Е-38 ÷ 3.4E+38)

double

8

1Е-37 ..  1E+37

точность мантиссы не менее 10 значащих цифр (типичным является диапазон 1.7Е-308 ÷ 1.7E+308)

long double

10

1Е-37 .. 1E+37

точность мантиссы не менее 10 значащих цифр (типичным является диапазон 3.4Е-4932 ÷ 1.1E+4932)

Логические значения

bool

1

true, false

 

Как видно из таблицы, базовые типы могут быть расширены с помощью специфика-торов (модификаторов) signed, unsigned, long, short.

 

2.2. Переменные языка С++

Переменная – поименованный участок памяти, в котором хранится значение. Имя (идентификатор) в языке С++ – совокупность букв, цифр и символа подчеркивания, начинающаяся с буквы или символа подчеркивания. В С++ строчные и прописные буквы считаются разными (т.е. abc и Авс – разные переменные). Имена в С++ бывают внешние и внутренние. Внешние имена обрабатываются во время внешнего процесса компоновки, это имена функций и глобальных переменных, которые совместно используются в различных исходных файлах. Все остальные имена – внутренние. Длина имени не ограничена, но ограничено количество значащих символов. В стандарте С89 значащими являются как минимум 6 символов внешнего имени и 31 – внутреннего. В стандарте С99 эти значения увеличены 31 – для внутреннего, 63 – для внешнего. Все переменные в языке Си должны быть описаны. Оператор описания переменных имеет вид:

тип список_переменных;

тип – один из типов, приведенных в табл. 2.1 или объявленных программистом, список_переменных – один или несколько идентификаторов, разделенных запятыми.

Например,

int a,bc,f;

float g,u,h12;

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


 

Массивы

Массив – совокупность данных одного и того же типа. Число элементов массива фиксируется при описании типа и в процессе выполнения программы не изменяется. Для доступа к элементу необходимо указать имя массива и его номер в квадратных скобках. Описание одномерного массива имеет вид:

тип имя_переменной [n];

где n – количество элементов в массиве; элементы в массиве нумеруются с нуля, таким образом, элементы в массиве нумеруются от 0 до n-1.

Например:

double a[25];

Описан массив a из 25 вещественных чисел (типа double), элементы нумеруются от 0 до 24 (a[0]…a[24]).

 

Многомерные массивы

В Си определены и многомерные массивы. Двумерный массив (матрицу) можно объявить так:

тип имя_переменной [n][m];

где n – количество строк в матрице(строки нумеруются от 0 до n-1), m – количество столбцов (столбцы нумеруются от 0 до m-1).

Например,

int h[10][15];

Описана матрица h, состоящая из 10 строк и 15 столбцов (строки нумеруются от 0 до 9, столбцы от 0 до 14).

Для обращения к элементу матрицы необходимо указать ее имя, и в квадратных скобках номер строки, а затем в квадратных скобках – номер столбца. Например, h[2][4] – элемент матрицы h, находящийся в третьей строке и пятом столбце.

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

тип имя_переменной [n1][n2]…[nk];

 

Строки

Строка – последовательность символов. Если в выражении встречается одиночный символ, он должен быть заключен в одинарные кавычки. При использовании в выражениях строка заключается в двойные кавычки. Признаком конца строки является нулевой символ ‘\0’.  Строки в Си можно описать, как массив символов (массив элементов типа char). Объявляя такой массив, следует предусмотреть место для хранения признака конца строки (‘\0’).

Например, описание строки из 25 символов должно выглядеть так:

char s[26];

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

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

1.                 Локальные – переменные, которые объявляются внутри функции и доступны только в ней.

Например:

int main()

{

float s;

s=4.5;

}

int f1()

{

int s;

s=6;

int f2() 

{

long int s;

s=25;

}

В функции main определена вещественная переменная s (типа float), ей присвоено значение 4.5, в функции f1 есть другая переменная s (типа int), ей присвоено значение 6, а в функции f2 есть еще одна переменная s (типа long int), ей присвоено значение 25.

2.                 Глобальные – переменные, которые описаны до всех функций, они доступны из любой функции.

Например:

#include <stdio.h>

float s;

int main()

{

s=4.5;

}

int f1()

{

s=6;

}

int f2()

{

s=25;

}

Определена глобальная переменная s (типа float), в функции main ей присваивается значение 4.5, в функции f1 – присваивается значение 6, а в функции f2 – присваивается значение 25.

3.                 Формальные параметры функций описываются в списке параметров функции. О формальных параметрах функциях будет рассказано при изучении темы «Функции в С++»

 

2.3. Константы в языке С++

Константы не изменяют своего значения в процессе выполнения программы.

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

const double pi=3.141592653589793

Таким образом, используя модификатор const можно определить неизменяемые переменные – константы.

В константе явно или не явно присутствует тип. По умолчанию константа будет принадлежать к типу наименьшего возможного размера. Однако, используя суффикс (символ после значения константы) можно явно указать тип. Если после вещественного числа в экспоненциальной форме присутствует символ F, то константа принадлежит к типу float, а если символ L – то к типу long double. Для целых чисел суффикс U обозначает unsigned, Llong.

Целые числа можно записывать в восьмеричной или шестнадцатеричной системе. Шестнадцатеричное число начинается с 0х (например, 0х80 – (80)16), восьмеричное с 0 (например, 025 – (25)8).

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

#define PI 3.141592653589793

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

#include <stdio.h>

#include <math.h>

#define PI 3.141592653589793

int main()

{

float a,b,c, alf,bet,s;

printf("A=");

scanf("%f",&a);

printf("B=");

scanf("%f",&b);

s=a*b/2;

c=pow(a*a+b*b,0.5);

alf=atan(a/b);

bet=PI/2-alf;

printf("\n A=%5.2f \t B=%5.2f \t C=%5.2f \n",a,b,c);

printf("\nS=%5.2f \t alf=%3.0f \t bet=%3.0f\n",

s,alf*180/PI,bet*180/PI);

}

 

2.4. Операции в языке С++

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

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

a=b; где a – имя переменной или элемента массива, b – значение как таковое, выражение, переменная, константа или функция.

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

Множественное присваивание

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

Например:

a=b=c=3.14159/6;

 

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

В табл. 2.2 представлены арифметические операции языка Си.

Таблица 2.2 . Основные операции языка С++

Операция

Действие

Тип операнда

Тип результата

+

Сложение

Целый, вещественный

Целый, вещественный

-

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

Целый, вещественный

Целый, вещественный

*

Умножение

Целый, вещественный

Целый, вещественный

/

Деление

Вещественный

Вещественный

/

Целочисленное деление

Целый

Целый

%

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

Целый

Целый

--

Декремент, уменьшение на 1

Целый

Целый

++

Инкремент, увеличение на 1

Целый

Целый

&

"и"

Целый

Целый

|

"или"

Целый

Целый

^

Исключающее "или"

Целый

Целый

~

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

Целый

Целый

<< 

Сдвиг влево

Целый

Целый

>> 

Сдвиг вправо

Целый

Целый

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

 

Операции увеличения (инкремента) и уменьшения (декремента)

В языке Си есть операции увеличения (++) и уменьшения (--) на единицу.

Оператор

p=p+1;

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

++p;

так и в постфиксной

p++;

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

Например,
x=12;
y=++x;

В результате в y будет храниться число 13.

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

Например,

x=12;

y=x++;

В результате в y будет храниться число 12.

 

Составное присваивание

К операторам составного присваивания относятся +=, -=, *=, /=.

Оператор x+=p; предназначен для увеличения x на величину p. Оператор x-=p; предназначен для уменьшения x на величину p. Оператор x*=p; предназначен для умножения x на p. Оператор x/=p; предназначен для деления x на p.

Операции целочисленной арифметики

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

·                     целочисленное деление /;

·                     остаток от деления  %.

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

11 % 4 = 3

11 / 4 = 2

7 % 3 = 1

7 / 3 = 2

26 / 5 = 5

26 % 5 = 1

 


Операции битовой арифметики

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

Арифметическое И (&). Оба операнда переводятся в двоичную систему, затем над ними происходит логическое поразрядное умножение операндов по следующим правилам.

1 & 1 = 1        1 & 0 = 0        0 & 1 =0         0 & 0 = 0

Рассмотрим пример использования арифметического И.

#include <stdio.h>

int main ()

{

int A, B;

   A=13;

   B=23;

   printf("\n%d\n", A & B)

}

Этот участок программы работает следующим образом. Число А=13 и В=23 переводятся в двоичное представление 0000000000001101 и 0000000000010111. Затем над ними поразрядно выполняется логическое умножение.

& 0000000000001101

   0000000000010111

   0000000000000101

Результат переводится в десятичную систему счисления, в нашем случае будет число 5. Таким образом, 13 & 23 = 5.

Арифметическое ИЛИ (|). Здесь также оба операнда переводятся в двоичную систему, после чего над ними происходит логическое поразрядное сложение операндов по следующим правилам.

1 | 1 = 1           1 | 0 = 1           0 | 1 =1            0 | 0 = 0

Например:

#include <stdio.h>

int main ()

{

int A, B;

   A=13;

   B=23;

   printf("\n%d\n", A | B)

}

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

|   0000000000001101

    0000000000010111

    0000000000011111

После перевода результата в десятичную систему имеем 13 | 23 =31.

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

1 ^ 1 = 0      1 ^ 0 = 1         0 ^ 1 =1          0 ^ 0 = 0

Арифметическое отрицание (~). Эта операция выполняется над одним операндом. Применение операции not вызывает побитную инверсию двоичного представления числа. Например, рассмотрим операцию not 13.

             0000000000001101

~a        1111111111110010

После перевода результата в десятичную систему получаем ~13=-14.

Сдвиг влево (M << L). Двоичное представление числа M сдвигается влево на L позиций. Рассмотрим пример использования операции <<, 17 << 3. Представляем число 17 в двоичной системе 10001, сдвигаем число на 3 позиции влево 10001000, в десятичной системе это число 136. 17 << 3 =136. Заметим, что сдвиг на один разряд влево соответствует умножению на 2, на два разряда – умножению на 4, на три – умножению на 8. Таким образом, операция M << L эквивалентна M.2L.

Cдвиг вправо (M >> L). В этом случае двоичное представление числа M сдвигается вправо на L позиций, что эквивалентно целочисленному делению числа M на .

Например, 25 >> 1=12, 25 >> 3= 3.

 

2.5. Логические операции и операции отношения

Логические операции выполняются над логическими значениями ИСТИНА (true) и ЛОЖЬ (false). В языке С ложью является 0, а истина – любое значение, отличное от нуля. В С++ появился тип bool. Результатами операций отношения (<, <=, >, >=, ==, ~=) или логической операции является ИСТИНА (true, 1) или ЛОЖЬ (false, 0). В Си определены следующие логические операции ИЛИ (||), И(&&), НЕТ (!) (см. табл. 2.3).

Таблица 2.3. Логические операции языка Си.

A

B

!A

A&&B

A||B

0

0

1

0

0

0

1

1

0

1

1

0

0

0

1

1

1

0

1

1

 


Операция ?

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

Условие? Выражение1: Выражение 2;

Операция работает так. Если Условие истинно (не равно 0), то результатом будет Выражение1, в противном случае Выражение2.

Например, оператор
y=x<0 ? –x: x;
записывает в переменную
y модуль числа х.

Операция явного приведения типа

Для приведения выражения к другому типу данных в С++ существует операция явного приведения типа:

(тип) выражение

Здесь тип – любой поддерживаемый в С++ тип данных.

Например,
x=5;
y=x/2;
z=(float) x/2;

В результате этого участка программы переменная y принимает значение 2 (результат целочисленного деления), а переменная z – 2.5/

 

2.6. Стандартные математические функции в языке С++

В  C++ определены стандартные функции над арифметическими операндами (табл. 2.4).

Таблица 2.4.  Некоторые стандартные математические функции в Си

Обозначение

Действие

abs(x)

Модуль целого числа

fabs(x)

Модуль вещественного числа

sin(x)

Функция синус

cos(x)

Функция косинус

tan(x)

Функция тангенс

atan(x)

Арктангенс [-pi/2;pi/2]

atan2(y,x)

Арктангенсy/xв диапазоне [-pi;pi]

sinh(x)

Гиперболический синус х

cosh(x)

Гиперболический косинус х

tanh(x)

Гиперболический тангенс x

exp(x)

ex

log(x)

Функция натурального логарифма ln(x), x>0

log10(x)

Функция десятичного логарифма log10(x), x>0

pow(x,y)

xy. Ошибка области определения, если х=0, y≤0 или x<0 и y – не целое

sqrt(x)

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

 

Определенную проблему представляет возведение Х в степень n. Функция pow не может возводить отрицательные числа в дробную степень. В этом случае можно воспользоваться формулой x^n=-exp(n*log(fabs(x)), которая программируется с помощью стандартных функций на языке С++ – -exp(n*log(fabs(x))) или –pow(fabs(x),y).

 

2.7. Ввод-вывод в языке Си

Вывод данных в языке Си с помощью функции printf

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

printf(s1, s2);

Здесь s1 – строка вывода, s2 – список выводимых переменных.

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

%[флаг][ширина][.точность][модификатор]тип.

 

Таблица 2.5.  Символы управления форматированием

Параметр

Назначение                   

Флаги

-

Выравнивание числа влево. Правая сторона дополняется пробелами. По умолчанию выравнивание вправо.

+

Перед числом выводится знак «+» или «-»

Пробел

Перед положительным числом выводится пробел, перед отрицательным – «-»

#

Выводится код системы счисления: 0 – перед восьмеричным числом, 0х (0Х) перед шестнадцатеричным числом.

Ширина

n

Ширина поля вывода. Если n позиций недостаточно, то поле вывода расширяется до минимально необходимого. Незаполненные позиции заполняются пробелами.

0n

То же, что и n, но незаполненные позиции заполняются нулями.

Точность

ничего

Точность по умолчанию

n

Для типов e, E, f выводить n знаков после десятичной точки

Модификатор

h

Для d, i, o, u, x, X тип short int

l

Для d, i, o, u, x, X тип long int

Тип

с

При вводе символьный тип char, при выводе один байт.

d

Десятичное int со знаком

i

Десятичное int со знаком

o

Восьмеричное int unsigned

u

Десятичное int unsigned

x, X

Шестнадцатеричное int unsigned, при х используются символы a-f, при Х – A-F.

f

Значение со знаком вида [-]dddd.dddd

e

Значение со знаком вида [-]d.dddde[+|-]ddd

E

Значение со знаком вида [-]d.ddddE[+|-]ddd

g

Значение со знаком типа e или f в зависимости от значения и точности

G

Значение со знаком типа e или F в зависимости от значения и точности

s

Строка символов

 


Некоторые специальные символы

Символ

Назначение

\b

Сдвиг текущей позиции влево

\n

Перевод строки

\r

Перевод в начало строки, не переходя на новую строку

\t

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

\’

Символ одинарной кавычки

\’’

Символ двойной кавычки

\?

Символ ?

Ввод данных в языке Си с помощью функции scanf

scanf(s1, s2);

Здесь s1 – список форматов вывода; s2 – список адресов вводимых переменных.

%тип

 scanf("%f%f",&a,&b);

 scanf("%f%d",&c,&d);

 printf("a=");

 scanf("%f",&a);

 

Вывод по - русски

#include <windows.h>

#include <iostream.h>

void main()

{

char name[14];

CharToOem("Пример", name);

cout<<name<<"\n";

}

 

Вывод данных в C++ с помощью функции cout

 

#include <iostream.h>

cout<<"X="<<X;

cout<<"x="<<x<<"y="<<y<<"\n";

cout<<"x="<<x<<"y="<<y<<endl;

cout<<''Summa =''<<x+y;

 


Ввод данных в C++ с помощью функции cin

 

#include <iostream.h>

cin>>a>>b;

cin>>c;

 

2.8.Указатели и массивы

Для работы с адресами памяти используются указатели.

int *a; // указатель на переменную целого типа float *addr_f; // указатель на переменную вещественного типа.
Размер памяти для переменной-указателя зависит от конфигурации машины. Конкретный адрес является указателем и получается с помощью операции & (адресации). Адресация не может применяться к переменным класса памяти register и к битовым полям структур /2/.
Разадресация * применяется для получения значения величины, указатель которой известен. Если значение указателя нулевое, то результат разадресации непредсказуем.
inta, x; int a[20]; double d;
рa= &a[5]; // это адрес 6-го элемента массива а
х = *рa; // x присваивается значение 6-го элемента массива а
if (x==*&x) cout<<"true\n"; d=*(double *)(&x);
// адрес х преобразуется к указателю на double и d=x.

Указатель - это переменная, содержащая адрес другой переменной. Указатели очень широко используются в языке Cи. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими способами. Так как указатель содержит адрес объекта, это дает возможность "косвенного" доступа к этому объекту через указатель /1/. Предположим, что X - переменная, например, типа int, а РX - указатель, созданный неким еще не указанным способом. Унарная операция & выдает адрес объекта, так что оператор РX = &X; присваивает адрес X переменной РX; говорят, что РX "указывает" на X. Операция & применима только к переменным и элементам массива, конструкции вида &(X-1) и &3 являются незаконными. Нельзя также получить адрес регистровой переменной.

Унарная операция * рассматривает свой операнд как адрес конечной цели и обращается по этому адресу, чтобы извлечь содержимое. Следовательно, если Y тоже имеет тип int, то Y = *РX; присваивает Y содержимое того, на что указывает РX. Последовательность РX = &X; Y = *РX; присваивает Y то же самое значение, что и оператор Y = X; Переменные, участвующие во всем этом, необходимо описать: int X, Y; int *РX; Описание указателя int *РX; должно рассматриваться как мнемоническое; оно говорит, что комбинация *РX имеет тип int. Это означает, что если РX появляется в контексте *РX, то это эквивалентно переменной тип int. Фактически синтаксис описания переменной имитирует синтаксис выражений, в которых эта переменная может появляться. Это замечание полезно во всех случаях, связанных со сложными описаниями.

Например, double atof(), *DР; говорит, что atof() и *DР имеют в выражениях значения типа double.

Указатели могут входить в выражения. Например, если РX указывает на целое X, то *РX может появляться в любом контексте, где может встретиться X. Оператор Y = *РX + 1; присваивает Y значение, на 1 большее значения X; printf("%d\n", *РX) печатает текущее значение X; D = sqrt((double) *РX) получает в D квадратный корень из X, причем до передачи функции sqrt значение X преобразуется к типу double. В выражениях вида Y = *РX + 1 унарные операции * и & связны со своим операндом более крепко, чем арифметические операции, так что такое выражение берет то значение, на которое указывает РX, прибавляет 1 и присваивает результат переменной Y.

Ссылки на указатели могут появляться и в левой части присваивания. Если РX указывает на X, то *РX = 0 полагает X равным нулю, *РX += 1 увеличивает его на единицу, как и выражение (*РX)++. Круглые скобки в последнем примере необходимы; если их опустить, то это выражение увеличит РX, а не ту переменную, на которую он указывает. Если РY - другой указатель на переменную типа int, то РY = РX копирует содержимое РX в РY, в результате чего РY указывает на то же, что и РX.

Массив рассматривается как набор элементов одного типа. Чтобы объявить массив, необходимо в описании поcле имени массива в квадратных скобках указать его размерность.

int а[5]; // массив, состоящий из 5 целых переменных
char sym[20]; // массив из 20 символьных переменных
double c[10]; // массив из 10 вещественных величин.

Индексы элементов изменяются от 0 до N-1, где N- размерность массива. Переменная типа массив является указателем на элементы массива.

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

float d_l[3][5], c_dim[5][3][3];

Элементы массива запоминаются в последовательных возрастающих адресах памяти. Хранятся элементы массива построчно. Для обращения к элементу массива вычисляется индексное выражение. Для a[i] индексное выражение равно *(а+ i) , где а - указатель, например, имя массива; i - целая величина, преобразованная к адресному типу; * - операция разадресации. Для двумерного массива b[i][j] индексное выражение: *(b+i+j).

В языке Cи существует сильная взаимосвязь между указателями и массивами, указатели и массивы следует рассматривать одновременно. Любую операцию, которую можно выполнить с помощью индексов массива, можно сделать и с помощью указателей. Вариант с указателями обычно оказывается более быстрым, но и несколько более трудным для непосредственного понимания. Описание int A[10] определяет массив размера 10, т.е. Набор из 10 последовательных объектов, называемых A[0], A[1], ..., A[9]. Запись A[I] соответствует элементу массива через I позиций от начала. Если РA - указатель целого, описанный как int *РA, то присваивание РA = &A[0] приводит к тому, что РA указывает на нулевой элемент массива A; это означает, что РA содержит адрес элемента A[0]. Теперь присваивание X = *РA будет копировать содержимое A[0] в X.

Если РA указывает на некоторый определенный элемент массива A, то по определению РA+1 указывает на следующий элемент, и вообще РA-I указывает на элемент, стоящий на I позиций до элемента, указываемого РA, РA+I на элемент, стоящий на I позиций после. Таким образом, если РA указывает на A[0], то *(РA+1) ссылается на содержимое A[1], РA+I - адрес A[I], *(РA+I) - содержимое A[I]. Суть определения "добавления 1 к указателю", а также его распространения на всю арифметику указателей, состоит в том, что приращение масштабируется размером памяти, занимаемой объектом, на который указывает указатель /2/.

Ссылку на A[I] можно записать в виде *(A+I). При анализе выражения A[I] в языке Cи оно немедленно преобразуется к виду *(A+I); эти две формы эквивалентны. Имеется одно различие между именем массива и указателем, которое необходимо иметь в виду. Указатель является переменной, так что операции РA=A и РA++ имеют смысл. Но имя массива является константой, а не переменной: конструкции типа A=РA или A++, или Р=&A будут незаконными. Когда имя массив передается функции, то на самом деле ей передается местоположение начала этого массива.

Внутри вызванной функции такой аргумент является точно такой же переменной, как и любая другая, так что имя массива в качестве аргумента действительно является указателем. Можно передать функции часть массива, если взять в качестве аргумента указатель начала подмассива. Например, если A - массив, то F(&A[2]) и F(A+2) передают функции F адрес элемент A[2], потому что и &A[2], и A+2 являются указательными выражениями, ссылающимися на третий элемент A.

Рассмотрим пример ввода/вывода значений элементов массива с помощью функций scanf и рrintf:

#include <stdio.h> // подключается файл с прототипами функций scanf и рrintf
void main(void)
{ int a[3]; int i; float b[4]; double c[2];
for (i=0; i<3; i++ )
{ scanf("%d ", &a[i] );
 рrintf("%d", a[i]); }
i=0; while (i < 4)
{scanf ("%f",b[i]);
рrintf("%g",b[i]); i++;}
i=0 ;
do
{ scanf("%lf ", &c[i]);
рrintf("%g", c[i]); i++;}
while(i< 2); } }

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

int *dim_i, n_dim_i;
n_dim_i =10;
dim_i =(int *)calloc(n_dim_i*sizeof(int)); . . .

Тогда освобождать память необходимо функцией free:

free(dim_i);

При использовании функицй calloc, malloc, free в текст программы требуется включить файл описаний этих функций alloc.h:

#include <alloc.h>

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

int *i_рtr; i_рtr=new int; // резервируется память под величину int
d_рtr = new double(3.1415); // то же, что и *i_рtr=3.1415
c_рtr = new char[str_len]; // область памяти для str_len символов

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

Примеры освобождения памяти оператором delete :

delete i_рtr; delete d_рtr; delete [str_len] c_рtr;

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

 

Символы и строки символов

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

char sym1, sym2, sym3, c;

В программе можно явно присвоить значение переменной типа char

sym1 = 'A'; sym2 = 'a';

sym3 = 'c'; c = '0x1B'; // ASCII символ

 

Ввод с клавиатуры:

scanf("%c ", &sym1); sym1 = getchar();

Вывод на экран:

рrintf ("%c", sym1); рutchar(sym1);

 

Фрагмент программы, выводящей на экране вводимые с клавиатуры символы до тех пор, пока не нажата клавиша Enter.

while((c = getchar( )) != '\ n' ) рutchar(с);

Esc-последовательности

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

\n - новая строка, \t - горизонтальная табуляция,

\v - вертикальная табуляция, \b - пробел,

- звонок, \` - одиночная кавычка,

\" - двойная кавычка, \\ - наклонная черта влево,

\ddd - восьмеричный код ASCII, \xdd -шестнадцатиричный код ASCII,

\c - означает обычный символ с.

 

 

2.9.Операторы

Простейшим оператором является оператор присваивания:
double x; int y; x=y; int fl=0, cfl=1;
#define MASK 0xfff
n=MASK;
j=i++;sum+=a[i]; q=(р==0)?(р+=1):(р+=2); l=b!!c;
d=0; a=b++=c++=d++; // значение а непредсказуемо

Обращения к функциям, вложенные операции присваивания, операции увеличения и уменьшения приводят к так называемым "побочным эффектам" - некоторые переменные изменяются в результате вычисления выражений. В любом выражении, в котором возникают побочные эффекты, могут существовать очень тонкие зависимости от порядка, в котором определяются входящие в него переменные. Примером типичной неудачной ситуации является оператор A[I] = I++; Возникает вопрос, старое или новое значение I служит в качестве индекса. Компилятор может поступать разными способами и в зависимости от своей интерпретации выдавать разные результаты.

Оператор if else организует ветвление в программе:

if (условие) оператор 1; else оператор 2;

Если условие истинно, то выполняется оператор 1, иначе - оператор 2. Оператор if else допускает любое число вложений, но проверка прекращается, если встречается истинное условие. Else - условие может опускаться.

if (num!=0 && 12/num==2) cout<<"YES!";
else cout<<"NO";

Более простое ветвление получается с помощью оператора switch:

switch (выражение)
{
case константа 1:
оператор; . . .; оператор;
break;
case константа 2:
оператор; . . .; оператор;
break;
. . . . . . . . . . .
case константа n:
оператор; . . .; оператор;
break;
default:
оператор; . . .; оператор;
}

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

Оператор for организует цикл:

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

выражение 3 (приращение) вычисляется после каждого прогона цикла;

выражение 1 (инициализация) вычисляется перед началом цикла;

выражение 2 (условие) - до и после каждого прогона цикла.

Оператор тела цикла выполняется до тех пор, пока истинно выражение 2.

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

Фрагмент программы вычисления суммы чисел от 0 до 9:

#include <iostream.h>
void main() {
for(int counter=0, int add_counter=0; counter<10; counter++, add_counter+=counter)
// "," используется для последовательного вычисления нескольких операторов
cout<<"Значение counter "<<counter<<endl;
// endl означает переход на новую строку
cout<<"Значение суммы "<<add_counter<<endl;
}

Для организации цикла с предусловием используется оператор while:

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

оператор;

Оператор тела цикла выполняется до тех пор, пока истинно выражение, записанное в скобках.

Выражение вычисляется до начала и после каждого прогона цикла. Цикл не выполняется ни разу, если выражение ложно (равно 0).

Фрагмент программы вычисления суммы чисел от 0 до 9:

#include <iostream.h>
void main()
{
int counter=0, add_counter=0;
while(counter++<10) {
// тело цикла состоит не из одного оператора, поэтому заключено в { }
cout<<"Значение counter"<<counter<<endl;
add_counter+=counter; }
cout<<"Значение суммы "<<add_counter<<endl;
}

 

Цикл с пост-условием do while имеет вид:

do
оператор;
while(условие);

Оператор тела цикла выполняется до тех пор, пока истинно условие. Условие проверяется после выполнения оператора тела цикла.

Фрагмент программы копирования строки в строку:

void main() {
char *р, *s;
s="string_s\0"; // строка заканчивается признаком конца - \0
do
*р++=*s++; // копирование строки
while(*s);
} // завершение цикла по концу строки

Оператор continue возвращает управление на начало цикла, пропуская стоящие после него операторы цикла.

Оператором break можно завершить цикл.

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

 

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

1.                 Какие операции можно применять к указателям?

2.                 Каков результат выполнения оператора c[i]=i++; ?

3.                 Для чего и как происходит динамическое распределение памяти?

4.                 Что такое Esc-последовательность?

5.                 Для чего используются индексные выражения?

6.                 Каким образом выделяется память под массивы?

7.                 Что общего между массивами и указателями?

8.                 Как располагаются в памяти элементы многомерного массива?

9.                 Чем отличаются циклы с пост- и предусловием?

10.            Каковы отличия действий операторов break и continue?

11.             Для чего применяется оператор switch?

12.            Приведите примеры использования операторов ветвления.

 

 

Глава3. Использование библиотечных

процедур СИ

 

3.1.О библиотеке СИ

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

1) интерфейс с функциями ОС (такими, как открытие и закрытие файлов);

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

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

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

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

 

3.2.Вызов библиотек СИ

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

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

1) включением стандартного #include-файла в программу. Многие процедуры требуют объявлений и определений. Для этого можно просто задать include-файлы, в которых определяются все требуемые объявления и определения;

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

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

 

3.2.Идентификация функций и макро

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

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

Наиболее важные различия между макро и функцией:

1) Некоторые макро не могут обрабатывать аргументы с побочными эффектами.

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

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

4) Библиотечные процедуры, выполняемые как макро, описываются при помощи препроцессорных директив в библиотечных include-файлах. Для использования библиотеки макро необходимо выбрать соответствующий файл, иначе макро не будет определен.

Пример:

#include <ctype.h>
int a='m';
a=toupper(a++);

В этом примере содержится процедура toupper из стандартной библиотеки Си. Здесь процедура toupper выполняется как макро, ее определение задано в <ctype.h>:

#define toupper (c)
((islower(c)? toupper(c):(c))

В приведенном определении используется условная операция (?:). В условном выражении аргумент "c" вычисляется дважды: первый раз - если он задан буквой нижнего регистра, второй раз – при возврате соответствующего значения. По этим причинам аргумент "a++" также должен быть вычислен дважды. В результате значение "a" должно увеличиться два раза и значение, выработанное по islower, отличается от значения, выработанного по toupper. Не все макро имеют такой побочный эффект. Для определения существования побочного эффекта нужно перед использованием макро подробно рассмотреть его определение.


3.3 Include-файлы

Большинство процедур Си используют макро, определения констант и типов. Они задаются в отдельных include-файлах. Чтобы использовать библиотечные процедуры, необходимо соединить специальный #include-файл (используя директиву препроцессора #include) с компилируемым исходным файлом. Содержимое каждого include-файла различно и зависит от специфики процедур. В общем include-файлы содержат комбинации следующих величин:

1) Определений манифестных констант.

Например, константы BUFSIZЕ, определяющей размеры буфера для операций буферизованного ввода/вывода. Сама константа определяется в include-файле <stdio.h>.

 

2) Определений типов.

Некоторые процедуры Си требуют в качестве аргументов структуры данных или возвращают значения структурных типов. В include-файлах задаются объявления этих структур. Например, большинство операций потокового ввода/вывода используют указатели на структуру типа FILE, объявленную в файле <stdio.h>.

 

3) Двух множеств объявления функций.

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

 

4) Макро определений.

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

 

3.4 Объявление функций

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

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

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

char * calloc(); ,

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

char * calloc (unsigned,unsigned); .

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

 

3.5 Проверка типов аргументов

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

Каждая функция библиотеки Си объявлена в одном или более include-файлах. Для каждой функции приводится два объявления: одно со списком типов аргументов и одно без него. Объявления функций задаются препроцессорным блоком #if define. Ограниченная проверка типов может выполняться только для функций, имеющих переменное число аргументов.

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

1) При вызове функций cprintf, cscanf, printf и scanf проверка типов аргументов выполняется только для первого аргумента, т.е. для аргумента, определяющего формат строки.

2) При вызове функций fprintf, fscanf, sprintf и sscanf проверка типов выполняется только для первых двух аргументов, т.е. для аргумента, определяющего файл или буфер, и аргумента формата строки.

3) При вызове open только два первых аргумента подлежат проверке, т.е. path-имя и флаг открытия.

4) При вызове sopen первые три аргумента подлежат проверке, т.е. path-имя, флаг открытия и распеделенный режим (sharing mode).

5) При вызове execl, execle, execlp и execlpe проверка типов выполняется для первых двух аргументов: для path-имени и первого указателя на аргумент.

6) При вызове spawnl, spawnle, spawnlp и spawnlpe проверка типов выполняется для первых трех аргументов: для флага режима, path-имени и первого указателя на аргумент.

 

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

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

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

При использовании функций, устанавливающих errno, можно сверить значение errno со значениями ошибок, определенных в include-файле <errno.h>, или же использовать функции perror и strerror. Если нужно распечатать сообщение о стандартной ошибке - используется perror; если сообщение об ошибке нужно расположить в строке, то используется strerror.

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

Если при этом возникла ошибка, то нужно использовать значение errno или функцию perror. Ошибки в математических функциях поддерживаются функцией matherr.

Ошибки в операциях с потоком проверяются функцией ferror.

Функция ferror обнаруживает, установлены ли индикаторы ошибки данного потока. Индикатор ошибки автоматически сбрасывается, если поток закрывается или выбрасывается, или вызывается функция clearerr для обнуления индикатора ошибки. Ошибки в низкоуровневых операциях ввода/вывода требуют установки переменной errno. Проверка конца файла (eof) данного потока осуществляется функцией feof. В низкоуровневых операциях ввода/вывода конец файла обнаруживается при помощи функции eof или когда операция считывания возвращает 0 как число прочитанных байтов.

 

3.7 Имена файлов и path-имена

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

 

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

1.                 Как осуществляется вызов библиотек СИ?

2.                 Укажите наиболее важные различия между макро и функцией.

3.                 Комбинации каких величин содержат include-файлы?

4.                 Как осуществляется объявление функций?

5.                 Для каких функций применяется ограниченная проверка?

6.                 Как осущетсвляется обработка ошибок?

7.                 В чем заключается основное использование имен файлов и path-имен?

 

Глава 4. Циклические алгоритмы,

их реализация в С++

4.1. Циклические алгоритмы

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

1. Цикл с предусловием

2. Цикл с постусловием

3. Безусловный циклический алгоритм

 

Задача 4.1. Составить таблицу значений функции y=2esin(3x)cos(4x) на отрезке [xn; xk] с шагом dx. Найти сумму положительных y и произведение отрицательных y.

 

 

 

4.2. Оператор цикла for

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

for(начальные_присваивания ; условие; приращение)

оператор;

for(начальные_присваивания ; условие; приращение)

{

оператор 1;

оператор 2;

оператор n;

}

for(i=in; i<=ik; i=i+di)

оператор;

for(i=in; i<=ik; i+=di)

оператор;

 

4.3. Оператор while

 

while (условие) оператор;

while условие

{

оператор 1;

оператор 2;

оператор n;

}

 

 

4.4. Оператор do-while

Цикл с постусловием реализован в виде оператора do-while. Структура этого оператора следующая:

do

{

оператор;

} while (условие);

 

 

4.5. Операторы break, continue

Оператор break осуществляет немедленный выход из циклов while, do-while и for. Его можно использовать только внутри циклов.

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

 

 

4.6. Решение задач с использованием циклов

Составить таблицу значений функции y=2esin(3x)cos(4x) на отрезке [xn;xk] с шагом dx. Найти сумму положительных y и произведение отрицательных y.

Текст программы с использованием оператора while (в стиле Паскаля)

#include <stdio.h>

#include <math.h>

int main()

{

float xn, xk, dx, x, y, s, p;

printf("Введите xn,xk,dx");

scanf("%f%f%f",&xn,&xk,&dx);

s=0;p=1;x=xn;

while(x<=xk)

{

y=2*exp(sin(3*x))*cos(4*x);

printf("X=%g\tY=%g\n",x,y);

if (y>=0) s+=y;

else p*=y;

x+=dx;

}

printf("S=%g\tP=%g\n",s,p);

}

Текст программы с использованием for (в стиле С).

#include <stdio.h>

#include <math.h>

int main()

{

float xn, xk, dx, x, y, s, p;

printf("Введите xn,xk,dx");

scanf("%f%f%f",&xn,&xk,&dx);

for(s=0,p=1,x=xn;x<=xk;x+=dx)

{

y=2*exp(sin(3*x))*cos(4*x);

printf("X=%g\tY=%g\n",x,y);

if (y>=0) s+=y;

else p*=y;

}

printf("S=%g\tP=%g\n",s,p);

}

И последняя версия программы с использованием цикла do-while.

#include <stdio.h>

#include <math.h>

int main()

{

float xn, xk, dx, x, y, s, p;

printf("Введите xn,xk,dx");

scanf("%f%f%f",&xn,&xk,&dx);

s=0;p=1;x=xn;

do

{

y=2*exp(sin(3*x))*cos(4*x);

printf("X=%g\tY=%g\n",x,y);

if (y>=0) s+=y;

else p*=y;

x+=dx;

}

while(x<=xk);

printf("S=%g\tP=%g\n",s,p);

}

 

Задача 4.2. Составить таблицу значений функции y=2esin(3x)cos(4x) на отрезке [xn;xk] с шагом dx. На экран вывести каждое третью пару x и y. Найти максимальное и минимальное значение y.

#include <iostream.h>

#include <math.h>

float f(float x, float a, float b, float c);

void main()

{

float xn, xk, dx, x, y, max,min;

int k=0;

cout<<"Vvedite xn, xk,dx\n";

cin>>xn>>xk>>dx;

for(x=xn;x<=xk;x+=dx)

{

y=f(x,2,3,4);

if (k%3==2)

cout<<"X="<<x<<",\tY="<<y<<endl;

k++;

if (k==1)

{

 max=y;

 min=y;

}

else

{

 if (y>max) max=y;

if (y<min) min=y;

}

}

cout<<endl<<"Max="<<max<<",\tMin="

<<min<<endl;

//while (getch()!='e');

getch();

}

float f(float x, float a, float b, float c)

{

float y;

y=a*exp(sin(b*x))*cos(c*x);

 return y;

}

 

#include "stdafx.h"

#include <iostream.h>

#include <math.h>

#include <conio.h>

float f(float x, float a, float b, float c);

{

float y;

y=a*exp(sin(b*x))*cos(c*x);

return y;

}

void main()

{

}

Vvedite xn, xk,dx

1 3

0.1

X=1.2,  Y=0.112422

X=1.5,  Y=0.722507

X=1.8,  Y=0.561794

X=2.1,  Y=-1.05619

X=2.4,  Y=-4.35526

X=2.7,  Y=-1.02516

X=3,   Y=2.54846

 

 

Max=2.54846,    Min=-4.35526

 

Задача 4.3. Вводится последовательность целых чисел, 0 – конец последовательности. Найти минимальное среди положительных, если таких значений несколько, определить, сколько их.

#include "stdafx.h"

#include <iostream.h>

#include <math.h>

#include <windows.h>

#include <conio.h>

void cout_rus(char text[])

{

char buffer[256];

CharToOem(text,buffer);

cout<<buffer;

}

int main(int argc, char* argv[])

{

int N,pr,k,min;

cout<<"\n N=";

cin>>N;

for(pr=k=0;N!=0;cout<<"N=",cin>>N)

if(N>0)

if (pr==0)

{

pr=1;

min=N;

k=1;

}

else

if (N<min)

{

min=N;

k=1;

}

else

if (N==min) k++;

if (pr)

{cout<<min;

cout_rus(" - минимальное число последовательности, таких чисел\t");

cout<<k<<endl;

}

else

cout<<"В последовательности нет положительных чисел";

 }

 

N=3

N=5

N=6

N=2

N=-4

N=5

N=2

N=-2

N=2

N=13

N=10

N=-6

N=2

N=0

2 - минимальное число последовательности, таких чисел   4

Press any key to continue

 

Задача 4.4. Дано натуральное число N. Определить самую большую цифру и ее позицию в числе (N=573863, наибольшей является цифра 8, ее позиция – четвертая слева).

#include <stdio.h>

#include <math.h>

int main()

{

long int N,M,kol=1;

int max,pos,i;

printf("\n Введите N>0\n");

scanf("%ld",&N);

M=N;

while(M/10>0)

{

kol++;

M/=10;

}

printf("В числе %ld %ld разрядов\n",

N,kol);

for(M=N,max=-1,pos=1,i=kol;i>1;i--)

{

if (M%10>max)

{

max=M%10;

pos=i;

}

M/=10;

}

printf("В числе %ld максимальная цифра %d, ее номер %d\n",

N,max,pos);

}

Задача 4.5. Определить количество простых чисел в интервале от N до M, где N и M – натуральные числа.

#include <stdio.h>

#include <math.h>

int main()

{

unsigned int N,M,i,j,pr,k;

do

{

printf("\n Введите N и M\n");

scanf("%u%u",&N,&M);

}while(M<N);

for(k=0,i=N;i<=M;i++)

{

for (pr=1,j=2;j<i/2;j++)

if (i%j==0)

{

pr=0;

break;

}

if (pr==1)

{

printf("\nЧисло %u - простое\n",i);

k++;

}

}

if (k)

printf("В интервале от %u до %u - %u простых чисел",

N,M,k);

else

printf("В интервале от %u до %u - нет простых чисел",

N,M);;

}

 

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

1.                 Что такое цикл?

2.                 Чем отличается цикл с предусловием от цикла с постусловием?

3.                 Что называется безусловным циклическим алгоритмом?

4.                 Укажите конструкцию оператора цикла for.

5.                 В чем состоит отличие оператора while от оператора do-while?

6.                 Основное назначение операторов break, continue.

 


Глава 5. Использование функций при программировании на С/С++ функциях

 

5.1. Структура программ

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

Заголовок_функции

{

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

}

Заголовок функции имеет вид

type имя_функции ([список параметров])

type – тип возвращаемого функцией значения;

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

 

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

директивы компилятора

...

Тип_результата f1(Список_переменных)

{

Операторы

}

Тип_результата f2(Список_переменных)

{

Операторы

}

...

Тип_результата fn(Список_переменных)

{

Операторы

}

int main(Список_переменных)

{

Операторы основной функции, среди которых могут операторы вызова функций f1, f2, ..., fn

}

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

директивы компилятора

...

Тип_результата f1(Список_переменных);

Тип_результата f2(Список_переменных);

...

Тип_результата fn(Список_переменных);

int main(Список_переменных)

{

Операторы основной функции, среди которых могут операторы вызова функций f1, f2, ..., fn

}

Тип_результата f1(Список_переменных)

{

Операторы

}

Тип_результата f2(Список_переменных)

{

Операторы

}

...

Тип_результата fn(Список_переменных)

{

Операторы

}

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

return значение;

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

 

5.2. Передача параметров в С/С++

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

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

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

 

5.3.Решения практических задач.

Задача 5.1. Вводится последовательность целых чисел, 0 – конец последовательности. Найти минимальное среди простых чисел и максимальное, среди чисел, не являющихся простыми.

Целое число называется простым, если оно делится нацело только на самого себя и единицу. Алгоритм проверки, что число N является простым состоит в следующем: если разделим N без остатка хотя бы на одно число в диапазоне от 2 до N/2, то число не является простым. Если не найдем ни одного делителя числа, число N – простое. Проверку является ли число N простым оформим в виде отдельной функции с именем prostoe. Входным параметром функции будет целое число N, функция будет возвращать значение 1, если число простое и 0 – в противном случае.

 

#include "stdafx.h"

#include <iostream.h>

 

int prostoe(int N)

{

int i,pr;

if (N<1) pr=0;

     else

for(pr=1,i=2;i<=N/2;i++)

if (N%i==0) {pr=0;break;}

return pr;

}

 

int main(int argc, char* argv[])

{

int kp=0,knp=0,min,max,N;

for (cout << "N=", cin>>N; N!=0; cout<<"N=", cin>>N)

if (prostoe(N))

{

kp++;

 if (kp==1) min=N;

 else if (N<min) min=N;

 }

 else

 {

knp++;

 if (knp==1) max=N;

 else if (N>max) max=N;

 }

if (kp>0) cout <<"min= "<<min<<"\t";

else cout <<"Net prostih";

if (knp>0) cout <<"max="<<max<<endl;

else cout <<"Net ne prostih";

return 0;

}

 

Задача 5.2. Вводится последовательность из N целых чисел, найти среднее арифметическое совершенных чисел и среднее геометрическое простых чисел.

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

При решении этой задачи понадобятся две функции:

-функция prostoe,

-функция soversh, которая определяет является ли число совершенным; входным параметром функции будет целое число N, функция будет возвращать значение 1, если число совершенным и 0 – в противном случае.

#include "stdafx.h"

#include <iostream.h>

#include <math.h>

int prostoe(int N)

{

int i,pr;

if (N<1) pr=0;

     else

for(pr=1,i=2;i<=N/2;i++)

if (N%i==0) {pr=0;break;}

return pr;

}

int soversh(int N)

{

int i,S;

if (N<1) return 0;

     else

for(S=0,i=1;i<=N/2;i++)

if (N%i==0) S+=i;

if (S==N) return 1;

else return 0;

}

int main(int argc, char* argv[])

{

int i,N,X,S,kp,ks;

long int P;

cout <<"N=";

cin>>N;

for(kp=ks=S=0,P=1,i=1;i<=N;i++)

{

     cout <<"X=";

     cin >> X;

     if (prostoe(X))

     {

         kp++;P*=X;

     }

     if (soversh(X))

     {

         ks++;S+=X;

     }

}

if (kp>0)

cout<<"SG="<<pow(P,(float)1/kp) <<endl;

else

cout<<"Net prostih";

if (ks>0)

 cout <<"SA="<<(float)S/ks<<endl;

else cout<<"Net soversh";

}

 

 

Задача 5.3. Дано натуральное число N. Определить самую большую цифру и ее позицию в числе (N=573863, наибольшей является цифра 8, ее позиция – четвертая слева).

 

 

#include "stdafx.h"

#include <stdio.h>

#include <math.h>

int kol_raz(int M)

{

int k=0;

while(M/10>0)

{

k++;

M/=10;

}

return k;

}

int main()

{

long int N,M,kol=1;

int max,pos,i;

printf("\n N=");

// Ввод числа N.

scanf("%ld",&N);

// Вычисление количества позиций

// в числе (kol).

kol=kol_raz(N);

printf("V chisle %ld - %ld razryadov\n",

N,kol);

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

// числе, и ее номера.

for(M=N, max=-1, pos=1, i=kol;i>1;i--)

{

if (M%10>max)

{

max=M%10;

pos=i;

}

M/=10;

}

// Вывод на экран максимальной цифры

// в числе, и ее номера.

printf("V chisle %ld maximalnaya tsifra %d,

 ee nomer %d\n",N,max,pos);

}

 

5.4. Рекурсивные функции в С/С++

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

long int factoial(int n) предназначена для вычисления факториала числа n.

 

 

#include "stdafx.h"

#include <iostream.h>

long int factorial(int n)

{

if (n<=1) return(n);

else

return(n*factorial(n-1));

}

int main()

{

int i;

long int f;

cout<<"i=";

cin>>i;

f=factorial(i);

cout<<i<<"!="<<f<<endl;

return 0;

}

2. float stepen(float a, int n) предназначена для возведения числа a в степень n.

#include "stdafx.h"

#include <iostream.h>

float stepen(float a, int n)

{

if (n==0) return(1);

else

     if (n<0) return(1/stepen(a,-n));

     else

         return(a*stepen(a,n-1));

}

int main()

{

int i;

float s,b;

long int f;

cout<<"b=";

cin>>b;

cout<<"i=";

cin>>i;

s=stepen(b,i);

cout<<"s="<<s<<endl;

return 0;

}

3. long int fibonachi(int n) предназначена для вычисления n-го числа Фибоначчи.

Если нулевой элемент последовательности равен 0, первый – 1, а каждый последующий равен сумме двух предыдущих, то это последовательность чисел Фибоначчи (0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... ).

#include "stdafx.h"

#include <iostream.h>

long int fibonachi(unsigned int n)

{

if ((n==0)||(n==1)) return(n);

else

return(fibonachi(n-1)+fibonachi(n-2));

}

int main(int argc, char* argv[])

{

int i;

long int f;

cout<<"i=";

cin>>i;

f=fibonachi(i);

cout<<"f="<<f<<endl;

return 0;

}

 

5.5. Область видимости переменных в функциях С/С++

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

1.Локальные переменные, которые объявляются внутри функции и доступны только в ней.

Например:

int main()

{

float s;

s=4.5;

}

int f1()

{

int s;

s=6;

}

int f2() 

{

long int s;

s=25;

}

В функции main определена вещественная переменная s (типа float), ей присвоено значение 4.5, в функции f1 есть другая переменная s (типа int), ей присвоено значение 6, а в функции f2 есть еще одна переменная s (типа long int), ей присвоено значение 25.

 

2.Глобальные – переменные, которые описаны до всех функций, они доступны из любой функции.

Например:

#include <stdio.h>

float s;

int main()

{

s=4.5;

}

int f1()

{

s=6;

}

int f2()

{

s=25;

}

Определена глобальная переменная s (типа float), в функции main ей присваивается значение 4.5, в функции f1 – присваивается значение 6, а в функции f2 – присваивается значение 25.

3.Формальные параметры функций описываются в списке параметров функции.

Рассмотрим особенности использования локальных и глобальных переменных в программах на С++:

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

2.Глобальные переменные объявляются вне любых функций и их областью видимостью  является весь файл.

3.Одно и тоже имя может использоваться при определении глобальной и локальной переменной. В этом случае в функции, где определена локальная переменная действует локальное описание, вне этой функции «работает» глобальное описание.

 

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

Рассмотрим это на примере

#include "stdafx.h"

#include <iostream.h>

float pr=100.678;

int prostoe (int n)

{

int pr=1,i;

if (n<0) pr=0;

else

for (i=2;i<=n/2;i++)

 if (n%i==0){pr=0;break;}

// Вывод локальной переменной

cout<<"local pr="<<pr<<endl;

// Вывод глобальной переменной

cout<<"global pr="<<::pr<<endl;

return pr;

}

int main()

{

int g;

cout<<"g=";

cin>>g;

if (prostoe(g)) cout<<"g – prostoe";

else cout<<"g – ne prostoe";

return 0;

}

 

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

 

g=7

local pr=1

global pr=100.678

g - prostoe

Press any key to continue

 

5.6. Перегрузка и шаблоны функций

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

Функция возведения в степень неопределена при 00 и при возведении отрицательного x в дробную степень n=k/m в случае четного m.

Пусть наша функция в этих случаях будет возвращать 0.

#include "stdafx.h"

#include <iostream.h>

#include <math.h>

float pow(float a, int k, int m)

{

cout<<"function 1\t";

if (a==0) return (0);

else

if (k==0) return(1);

else

if (a>0)

return(exp((float)k/m*log(a)));

else

if (m%2!=0)

return (-(exp((float)k/m*log(-a))));

}

float pow(float a, int n)

{

if (a==0)

{cout<<"function 2\t";return (0);}

else

if (n==0)

{cout<<"function 2\t";return (1);}

else

if (n<0) return(1/pow(a,-n));

else

return(a*pow(a,n-1));

}

int pow(int a, int n)

{

if (a==0)

 {cout<<"function 3\t";return (0);}

else

if (n==0)

{cout<<"function 3\t";return (1);}

else

if (n<0) return(1/pow(a,-n));

else

return(a*pow(a,n-1));

}

int main()

{

float a;

int k,n,m;

cout<<"a=";

cin>>a;

cout<<"k=";

cin>>k;

cout<<"s="<<pow(a,k)<<endl;

cout<<"s="<<pow((int)a,k)<<endl;

cout<<"a=";

cin>>a;

cout<<"k=";

cin>>k;

cout<<"m=";

cin>>m;

cout<<"s="<<pow(a,k,m)<<endl;

return 0;

}

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

a=5.2

k=3

function 2      s=140.608

function 3      s=125

a=-8

k=1

m=3

function 1      s=-2

Press any key to continue

 

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

Шаблон – это особый вид функций, который начинается со служебного слова template, за которым в угловых скобках (<>) следует список используемых в функции типов данных. Каждый тип предваряется служебным словом class. Можно сказать, что в случае шаблона в качестве параметров выступают не только переменные, но их типы.

Рассмотрим пример шаблона поиска наименьшего из четырех чисел.

#include "stdafx.h"

#include <iostream.h>

//Определяем абстрактный тип данных с

// помощью служебного слова Type

template <class Type>

// Определяем функцию с использованием

// типа данных Type

Type minimum(Type a, Type b, Type c, Type d)

{

Type min=a;

if (b<min) min=b;

if (c<min) min=c;

if (d<min) min=d;

return min;

}

int main()

{

int ia,ib,ic,id,mini;

float ra,rb,rc,rd,minr;

cout<<"Vvod 4 thelih chisla\t";

cin>>ia>>ib>>ic>>id;

//Вызов функции minimum, в которую

// передаем 4 целых значениями

mini=minimum(ia,ib,ic,id);

cout<<"\n"<<mini<<"\n";

cout<<"Vvod 4 vecshestvenih chisla\t";

cin>>ra>>rb>>rc>>rd;

//Вызов функции minimum, в которую

// передаем 4 вещественных значениями

minr=minimum(ra,rb,rc,rd);

cout<<"\n"<<minr<<"\n";

return 0;

}

5.7. Использование значений формальных параметров по умолчанию

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

#include "stdafx.h"

#include <iostream.h>

float stepen(float a, int n=3)

{

if (n==0) return(1);

else

         if (n<0) return(1/stepen(a,-n));

         else

             return(a*stepen(a,n-1));

}

int main()

{

int a;

long int f;

cout<<"a=";

cin>>a;

f=stepen(a,5);

cout<<"f="<<f<<endl;

f=stepen(a);

cout<<"f="<<f<<endl;

return 0;}

 

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

1.                 Что называется подпрограммой?

2.                 Из чего состоит описание функции?

3.                 Как осуществляется передача параметров в С/С++?

4.                 Чем формальные параметры отличаются от фактических?

5.                 Что понимается под рекурсией в программировании?

6.                 Укажите основные реурсивные функции в С/С++.

7.                 Укажите основные классы облати видимости переменных в функциях С/С++?

8.                 Что называется перегрузкой функции?

9.                 Что такое шаблон функции?

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

 

 

Глава 6. Обработка массивов в языке Си++

6.1. Описание  массивов

Массив – структурированный тип данных, состоящий из фиксированного числа элементов одного типа

Массив X

12.1

0.13

-1.5

0

21.9

-3.7

5.0

121.7

0-элемент массива

1-элемент массива

2-элемент массива

3-элемент массива

4-элемент массива

5-элемент массива

6-элемент массива

7-элемент массива

Одномерный массив описывают так:

тип имя_переменной [n];

где n – количество элементов в массиве, причем нумерация начинается с нуля: от 0 до n–1.

Например:

int х[10];

float g[25];

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

Например,

-1

3

2

0

-8

5

1

-2

0

1

2

3

4

5

6

7

  x[0]                                                            x[4]

Двумерный массив (матрицу) можно объявить так:

тип имя_переменной [n][m];

где n – количество строк (от 0 до n-1), m – количество столбцов (от 0 до m-1).

Например,   double m[3][4];

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

Например, a[1][2] – элемент матрицы a, находящийся в первой строке и втором столбце.

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

Например,

float a[5]={1.2,(float)3/4,

    5./6,6.1,7.8};

 

6.2. Ввод элементов массива

 

 

Реализация в СИ++

1.

#include <stdio.h>

#include <math.h>

int main()

{

float x[10];

int i,n;

printf("\n N=");

scanf("%d",&n);

printf("\n Введите массив X \n");

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

scanf("%f",&x[i]);

}

2.

#include <stdio.h>

#include <math.h>

int main()

{

float x[10],b;

int i,n;

printf("\n N=");

scanf("%d",&n);

printf("\n Введите массив X \n");

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

{

scanf("%f",&b);

x[i]=b;

}

}

 

6.3. Вывод элементов массива

При организации вывода элементов массива можно использовать специальные символы \t \n.

 printf("\n Массив X\n");

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

printf("%g\t",x[i]);

printf("\n");

 

6.4. Основные алгоритмы обработки массивов

 

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

обработка X[i];

 

 

 

 

 

 

 

 

 

 

Алгоритм обработки массива

 

 

Алгоритм вычисления суммы элементов массива

 

 

 

for(s=0,i=0;i<n;i++)

s+=X[i];

 

Алгоритм вычисления произведения элементов массива

 

for(P=1,i=0;i<n;i++)

P*=X[i];

 

Аогримт поиск максимального элемента массива и его номера

 

for(max=X[0],nmax=0,i=1;i<n;i++)

if (X[i]>max)

{

max=X[i]; nmax=i;

}

Алгоритм удаления элемента из массива

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

X[3]=X[4];

X[4]=X[5];

X[5]=X[6];

В общем случае:

Удаляем элемент с номером M из массива X, в котором N элементов.

for(i=M;i<=N-2;i++) X[i]=X[i+1];N--

 

 

После удаления следует учитывать, что изменилась нумерация (все номера уменьшились на 1) элементов, начиная с номера M, поэтому если удалять несколько элементов подряд не надо переходить к следующему.

ПРИМЕР. Удалить элементы с 4-го по 8-й в массиве из N элементов

for(j=1;j<=5;j++,N--)

for(i=3;i<=N-2;i++)  X[i]=X[i+1];

int main()

{

floxt x[20];

int i,j,n;

cout<<"n=";

cin>>n;

cout<<"Massiv x\n";

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

cin>>x[i];

for(j=1;j<=5;j++)

for(i=3;i<=n-1-j;i++)

x[i]=x[i+1];

cout<<"Massiv x\n";

for(i=0;i<n-5;i++)

cout<<"x("<<i<<")="<<x[i]<<"\t";

cout<<endl;

return 0;

}

 

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

n=

15

Massiv x

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Massiv x

x(0)=1  x(1)=2  x(2)=3  x(3)=9  x(4)=10 x(5)=11 x(6)=12 x(7)=13 x(8)=14 x(9)=15

 

Упорядочение элементов массива

Сортировка выбором

 

int main()

{

float b,max,a[20];

int i,j,n,k,nom;

cout<<"n=";

cin>>n;

cout<<"Massiv a\n";

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

cin>>a[i];

k=n;

for(j=0;j<=k-2;j++)

{

max=a[0];nom=0;

for(i=1;i<k;i++)

if (a[i]>max)

{

max=a[i];

nom=i;

}

b=a[k-1];

a[k-1]=a[nom];

a[nom]=b;

k--;

}

cout<<"Massiv a\n";

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

cout<<"a("<<i<<")="<<a[i]<<"\t";

cout<<endl;

return 0;

}

Сортировка методом пузырька

 

 

int main()

{

float b,max,a[20];

int i,j,n,k,nom;

cout<<"n=";

cin>>n;

cout<<"Massiv a\n";

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

cin>>a[i];

for(j=1;j<=n-1;j++)

for(i=0;i<=n-1-j;i++)

if (a[i]>a[i+1])

{

b=a[i];

a[i]=a[i+1];

a[i+1]=b;

}

cout<<"Massiv a\n";

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

cout<<"a("<<i<<")="<<a[i]<<"\t";

cout<<endl;

return 0;

}

 

Запись положительных элементов массива A в массив B

int main()

{

float a[20],b[20];

int i,n,k;

cout<<"n=";

cin>>n;

cout<<"Massiv a\n";

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

cin>>a[i];

for(k=i=0;i<n;i++)

if (a[i]>0)  b[k++]=a[i];

cout<<"Massiv b\n";

for(i=0;i<k;i++)     cout<<"b("<<i<<")="<<b[i]<<"\t";

cout<<endl;

return 0;

}

 

 

Вставка элемента b в упорядоченный массив X, не нарушив его упорядоченности

Пусть массив Х(N) упорядочен по возрастанию, необходимо в него вставить элемент b, не нарушив упорядоченности массива.

 

int main()

{

float x[20],b;

int i,j,n;

cout<<"n=";

cin>>n;

cout<<"Massiv x\n";

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

cin>>x[i];

cout<<"b=";

cin>>b;

if (b>=x[n-1])

x[n]=b;

else

{

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

if (x[i]>b)

{j=i;break;}

for(i=n;i>j;i--)

x[i]=x[i-1];

x[j]=b;

}

n++;

cout<<"Massiv x\n";

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

cout<<"x("<<i<<")="<<

x[i]<<"\t";

cout<<endl;

return 0;

}

 

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

1.                 Что такое массив?

2.                 В чем оличие одномерных и многомерных массивов?

3.                 Как осуществляется ввод и вывод элементов массива?

4.                 Перечислите основные алгоритмы обработки массивов.

5.                 Как осуществляется алгоритм вычисления суммы элементов массива?

6.                 Как осуществляется алгоритм вычисления произведения элементов массива?

7.                 Охарактеризуйте алгоритм поиска максимального элемента массива и его номера.

8.                 Охарактеризуйте алгоритм удаления элемента из массива.

9.                 Перечислите основные способы упорядочивания элементов массива.

10.            В чем заключается суть алгоритма сортировки выбором?

11.            Охарактеризуйте сортировку массива методом пузырька.

12.            Как осуществляется запись положительных элементов массива A в массив B?

 

Глава 7. Указатели, динамические массивы

Как видно из рассмотренных ранее примеров, в Си++ массивы статические, их размер задается при описании. Это не всегда удобно, кроме того, при решении некоторых задач заранее неизвестен размер формируемого массива.

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

 

7.1. Указатели в С++

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

Указатель описывается следующим образом:

type * name;

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

Например:

int *i;

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

float *x,*z;

Объявляем указатели с именами x и z, которые указывают на переменные типа float.

 

7.2. Операции * и & при работе с указателями

При работе с указателями в основном используются операции & и *. Операция & возвращает адрес своего операнда.

Например, если объявлена переменная a следующим образом:

float a;

то оператор

adr_a=&a;

записывает в переменную adr_a адрес переменной a, переменная adr_a должна быть указателем на тип float. Ее следует описать следующим образом:

float *adr_a;

Операция * выполняет действие, обратное операции &. Она возвращает значение переменной, хранящееся по заданному адресу.

Например, оператор

a=*adr_a;

записывает в переменную a вещественное значение, хранящееся по адресу adr_a.

 

7.3. Операция присваивания указателей

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

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

#include <stdio.h>

#include <math.h>

int main()

{

float PI=3.14159,*p1,*p2;

p1=p2=&PI;

printf("По адресу p1=%p хранится *p1=%g\n",p1,*p1);

printf("По адресу p2=%p хранится *p2=%g\n",p2,*p2);

}

В этой программе определены: вещественная переменная PI=3.14159 и два указателя на тип float p1 и p2. Затем в указатели p1 и p2 записывается адрес переменной PI. Операторы printf выводят на экран адреса p1 и p2 и значения, хранящиеся по этим адресам. Для вывода адреса используется спецификатор типа %p. В результате работы этой программы в переменных p1 и p2 будет храниться значение одного и того же адреса, по которому хранится вещественная переменная PI=3.14159.

 

По адресу p1=0012FF7C хранится *p1=3.14159

По адресу p2=0012FF7C хранится *p2=3.14159

 

Если указатели ссылаются на различные типы, то при присваивании значения одного указателя другому, необходимо использовать преобразование типов. Без преобразования можно присваивать любому указателю указатель void *. Рассмотрим пример работы с указателями различных типов.

#include <stdio.h>

#include <math.h>

int main()

{

float PI=3.14159,*p1;

double *p2;

//В переменную p1 записываем адрес PI

p1=&PI;

//указателю на double присваиваем

//значение, которое ссылается на тип

//float.

p2=(double *)p1;

printf("По адресу p1=%p хранится *p1=%g\n",p1,*p1);

printf("По адресу p2=%p хранится *p2=%e\n",p2,*p2);

}

 

По адресу p1=0012FF7C хранится *p1=3.14159

По адресу p2=0012FF7C хранится *p2=2.642140e-308

 

В указателях p1 и p2 хранится один и тот же адрес, но значения, на которые они ссылаются, оказываются разными. Это связано с тем, указатель типа *float адресует 4 байта, а указатель *double – 8 байт. После присваивания p2=(double *)p1; при обращении к *p2 происходит следующее: к переменной, хранящейся по адресу p1, дописывается еще 4 байта из памяти. В результате значение *p2 не совпадает со значением *p1.

А что произойдет в результате следующей программы?

#include <stdio.h>

#include <math.h>

int main()

{

double PI=3.14159,*p1;

float *p2;

p1=&PI;

p2=(float *)p1;

printf("По адресу p1=%p хранится *p1=%g\n",p1,*p1);

printf("По адресу p2=%p хранится *p2=%e\n",p2,*p2);

}

После присваивания p2=(double *)p1; при обращении к *p2 происходит следующее: из переменной, хранящейся по адресу p1, выделяется только 4 байта. В результате и в этом случае значение *p2 не совпадает со значением *p1.

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

 

float *p;

p – адрес;

*p – значение, хранящееся по адресу p.

 

float p;

&p – адрес;

p – значение, хранящееся по адресу &p.

 

 

7.4. Арифметические операции над адресами

Над адресами в языке Си определены следующие операции:

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

-вычитание, можно вычитать указатели или вычитать из указателя целое число.

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

 

double *p1;

float *p2;

int *i;

p1++;

 

p2++;

i++;

}

Операция p1++ увеличивает значение адреса на 8, операция p2++ увеличивает значение адреса на 4, а операция i++ на 2. Операции адресной арифметики выполняются следующим образом:

-операция увеличения приводит к тому, что указатель будет слаться на следующий объект базового типа (для p1 – это double, для p2 – float, для i – int);

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

-после операции p1=p1+n, указатель будет передвинут на n объектов базового типа; p1+n как бы адресует n-й элемент массива, если p1 – адрес начала массива.

 

7.5. Использование адресов и указателей при работе с массивами.

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

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

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

1.Описать указатель (например, переменную p) определенного типа.

2.Начиная с адреса, определенного указателем, с помощью функций calloc, malloc или операции new выделить участок памяти определенного размера. После этого p будет адресом первого элемента выделенного участка оперативной памяти (0-й элемент массива), p+1 будет адресовать – следующий элемент в выделенном участке памяти (1-й элемент динамического массива), …, p+i является адресом i-го элемента. Необходимо только следить, чтобы не выйти за границы выделенного участка памяти.

 

К i-му элементу динамического массива p можно обратиться одним из двух способов

 

*(p+i) или p[i]

3.Когда участок памяти будет не нужен, его можно освободить с помощью функции free(), операции delete.

Перед подробным описанием работы с динамическими переменными, рассмотрим функции calloc, malloc, realloc и free и операции new и delete.

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

void *malloc(n);

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

 

ПРИМЕР. Найти сумму элементов динамического массива вещественных чисел.

#include "stdafx.h"

#include <iostream.h>

#include <malloc.h>

int main()

{

int i,n;

float *a,s;

cout<<"n=";

cin>>n;

a=(float *)malloc(n*sizeof(float));

cout << "Vvedite massiv A";

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

// cin>>a[i];

cin>>*(a+i);

for(s=0,i=0;i<n;i++)

//s+=a[i];

s+=*(a+i);

cout << "S="<<s;

free(a);

return 0;

}

 

Кроме функции malloc для выделения памяти в Си есть функция calloc. Ее особенностью является обнуление всех выделенных элементов. Обращение к функции имеет вид:

void *calloc (num, size);

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

 

#include "stdafx.h"

#include <iostream.h>

#include <malloc.h>

int main()

{

    int i,n;

    float *a,s;

    cout<<"n=";

    cin>>n;

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

    cout << "Vvedite massiv A";

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

         cin>>*(a+i);

    for(s=0,i=0;i<n;i++)

         s+=*(a+i);

    cout << "S="<<s;

    free(a);

    return 0;

}

 

Функция realloc изменяет размер выделенной ранее памяти, обращение к ней имеет вид:

char *realloc(void *p, size);

Функция изменяет размер участка памяти, на который указывает p, новый размер участка памяти size. Если при этом пришлось изменять месторасположение участка памяти, то новое его месторасположение и возвращается в качестве результата. Если в качестве p передается NULL, то функция realloc работает аналогично функции malloc.

Для освобождения выделенной памяти используется функция free. Обращение к ней имеет вид

void free( void *p);

Освобождает память, выделенную память, p – указатель на участок память, ранее выделенный функциями calloc, malloc или realloc.

Для выделения памяти в С++ есть еще операция new.

Для выделения памяти под n элементов типа float можно поступить так.

int n;

float *a;

cin>>n;

a=new float[n];

int main()

{

int i,n;

float *a,s;

cout<<"n=";

cin>>n;

a=new float[n];

cout << "Vvedite massiv A";

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

// cin>>a[i];

cin>>*(a+i);

for(s=0,i=0;i<n;i++)

//s+=a[i];

s+=*(a+i);

cout << "S="<<s;

delete [] a;

return 0;

}

 

Что произойдет, если указатель (адрес) передать в качестве параметра в функцию?

ЭТО ПОЗВОЛИТ ВЕРНУТЬ ВСЕ ЗНАЧЕНИЯ, ХРАНЯЩИЕСЯ ПО ЭТОМУ АДРЕСУ В ГЛАВНУЮ ФУНКЦИЮ

 

7.6. Примеры программ

ЗАДАЧА 1. В заданном массиве найти длину самой длинной серии элементов, состоящей из единиц.

Fl были (1) или нет (0) серии элементов из единиц, k-длина текущей серии, max – самая длинная серия.

intmain()

{

int *x,max,i,k,fl,n,b;

printf("\n n=");

scanf("%d",&n);

x=new int[n];

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

{

printf("\n x(%d)=",i);

scanf("%d",&b);

*(x+i)=b;

}

for (k=1,fl=0,i=0;i<n-1;i++)

{

if (*(x+i)==1 && *(x+i+1)==1)

k++;

else if (k!=1)

{

if (!fl)

{

max=k;

fl=1;

k=1;

}

else if (k>max)

max=k;

k=1;

}

}

if (fl==0)

printf("V massive net seriy iz 1\n");

else

{

if (k>max)

max=k;

printf("\n %d",max);

}

delete [] x;

return 0;

}

 

ЗАДАЧА 2. Из массива целых чисел удалить все простые числа меньшие среднего арифметического. Полученный массив упорядочить по возрастанию.

Кроме главной функции main() при решении этой задачи необходимо написать следующие функции:

-bool prostoe(int n), которая будет проверять является ли число n простым;

-void udal(int *x, int m, int *n) функция удаления элемента с номером m в массиве x из n элементов.

-void upor(int *x, int N, bool pr=true) функция упорядочивания массива по возрастанию (если pr=true) или по убыванию (если pr=false);

-float sr_arifm(int x[], int n) функция вычисления среднего арифметического в массиве x из n элементов.

Блок-схема функции prostoe

Блок-схема функции udal

 

 

 

 

Блок-схема функции sr_arifm

 

Блок-схема функции upor

#include "stdafx.h"

#include <iostream.h>

#include <malloc.h>

bool prostoe(int n)

{

bool pr;

int i;

for(pr=true, i=2;i<=n/2;i++)

if(n%i==0)

{pr=false;break;}

return(pr);

}

void udal(int *x, int m, int *n)

{

int i;

for(i=m;i<=*n-2;i++)

*(x+i)=*(x+i+1);

 --*n;

realloc((int *)x,*n*sizeof(int));

}

void upor(int *x, int n, bool pr=true)

{

int i,j,b;

if (pr)

{

for(j=1;j<=n-1;j++)

for(i=0;i<=n-1-j;i++)

if (*(x+i)>*(x+i+1))

{

b=*(x+i);

*(x+i)=*(x+i+1);

*(x+i+1)=b;

}

}

else

for(j=1;j<=n-1;j++)

for(i=0;i<=n-1-j;i++)

if (*(x+i)<*(x+i+1))

{

b=*(x+i);

*(x+i)=*(x+i+1);

*(x+i+1)=b;

}

}

float sr_arifm(int x[], int n)

{

int i;

float s=0;

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

s+=x[i];

if (n>0)

return(s/n);

else return 0;

}

int main()

{

int *a,n,i;

float sr;

cout<<"n=";

cin>>n;

a=(int *)calloc(n,sizeof(int));

cout << "Vvedite massiv A\n";

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

cin>>*(a+i);

sr=sr_arifm(a,n);

cout<<"sr="<<sr<<endl;

for(i=0;i<n;)

{

if(prostoe(*(a+i))&& *(a+i)<sr)

udal(a,i,&n);

else i++;

}

cout << "Massiv A\n";

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

cout<<*(a+i)<<"\t";

cout<<endl;

upor(a, n);

cout << "Upor Massiv A\n";

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

cout<<*(a+i)<<"\t";

cout<<endl;

free(a);

return 0;

}

 

n=10

Vvedite massiv A

6 20 5 3 10 301 17 11 6 8

sr=38.7

Massiv A

6   20  10  301 6   8

Upor Massiv A

6   6   8   10  20  301

Press any key to continue

 

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

1.                 Какие массивы называются динамическими?

2.                 Что называется указателем?

3.                 Каким образом описывается указатель?

4.                 Укажите основные операции, используемые при работе с указателями.

5.                 Охарактеризуйте операцию присваивания указателей.

6.                 Охарактеризуйте арифметические операции над адресами.

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

 

Глава 8. Матрицы. Многомерные массивы

В Си определены и многомерные массивы. Двумерный массив (матрицу) можно объявить так:

тип имя_переменной [n][m];

где n – количество строк в матрице(строки нумеруются от 0 до n-1), m – количество столбцов (столбцы нумеруются от 0 до m-1).

Например,

int h[10][15];

Описана матрица h, состоящая из 10 строк и 15 столбцов (строки нумеруются от 0 до 9, столбцы от 0 до 14).

Для обращения к элементу матрицы необходимо указать ее имя, и в квадратных скобках номер строки, а затем в квадратных скобках – номер столбца. Например, h[2][4] – элемент матрицы h, находящийся в третьей строке и пятом столбце.

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

тип имя_переменной [n1][n2]…[nk];

 

 

 

 

8.1.Блок-схемы основных алгоритмов обработки матриц

Блок-схема ввода матрицы

Блок-схема вывода матрицы

 

8.2. Свойства матриц

Рассмотрим некоторые свойства матриц:

·                     если номер строки элемента совпадает с номером столбца (i = j), это означает что элемент лежит на главной диагонали матрицы;

·                     если номер строки  превышает номер столбца (i > j), то элемент находится ниже главной диагонали;

·                     если номер столбца больше номера строки (i < j), то элемент находится выше главной диагонали.

·                     элемент лежит на побочной диагонали, если его индексы удовлетворяют равенству i + j 1 = n;

·                     неравенство i + j + 1 < n характерно для элемента находящегося выше побочной диагонали;

·                     соответственно, элементу лежащему ниже побочной диагонали соответствует выражение i + j + 1 > n.

 

 

Найти сумму элементов матрицы, лежащих выше главной диагонали

 

 

 

#include "stdafx.h"

#include <stdio.h>

void main(int argc, char* argv[])

{

float b,a[20][20],s;

int i,j,n,m;

printf("n=");scanf("%d",&n);

printf("m=");scanf("%d",&m);

printf("\n Vvedite A \n");

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

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

{

scanf("%g",&b);

a[i][j]=b;

}

printf("\n Matrica A\n");

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

{

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

printf("%g\t",a[i][j]);

printf("\n");

}

for(s=0, i=0;i<n;i++)

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

if (i<j) s+=a[i][j];

printf("\nS=%g\n",s);

}

 

Поиск максимального элемента и его индексов

 

8.3.Динамические матрицы

1-й способ работы с динамическими матрицами.

При работе с динамическими матрицами следует помнить, что выделенный участок памяти под матрицу A(N,M) представляет собой участок памяти размером NxM элементов.

A=(тип *) calloc(n*m, sizeof(тип))

или

A=(тип *) malloc(n*m*sizeof(тип))

Поэтому для обращения к элементу Ai,j необходимо, но номеру строки i и номеру столбца j вычислить номер этого элемента k в динамическом массиве. Учитывая, то что в массиве элементы нумеруются с нуля k=i.M+j.

a[i][j] *(a+i*m+j)

 

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

#include "stdafx.h"

#include <iostream.h>

#include <malloc.h>

int main(int argc, char* argv[])

{

float *a;

int i,j,n,k=0;

cout<<"n="; cin>>n;

cout<<"Vvod matrici"<<endl;

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

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

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

cin>>*(a+i*n+j);

cout<<"Matrica"<<endl;

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

{

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

cout<<*(a+i*n+j)<<"\t";

cout<<endl;

 }

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

{

if (*(a+i*n+i)>0) k++;

if( *(a+i*n+n-i-1)>0) k++;

}

cout<<"k="<<k<<endl;

for(i=1;i<n-1;i++)

{

if (*(a+0*n+i)>0 ) k++;

if (*(a+i*n+0)>0) k++;

if (*(a+(n-1)*n+i)>0) k++;

if (*(a+i*n+n-1)>0) k++;

}

cout<<"k="<<k<<endl;

if (n%2!=0 && *(a+(n-1)/2*n+(n-1)/2)>0) k--;

cout<<"k="<<k<<endl;

free(a);

return 0;

}

2-й способ работы с динамическими матрицами.

Основан на использовании двойного указателя, указателя на указатель.

Перед этим давайте вспомним использование операции new при работе с динамическими массивами.

int main()

{

int i,n;

float *a,s;

cout<<"n=";

cin>>n;

a=new float[n];

cout << "Vvedite massiv A";

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

// cin>>a[i];

cin>>*(a+i);

for(s=0,i=0;i<n;i++)

//s+=a[i];

s+=*(a+i);

cout << "S="<<s;

delete [] a;

return 0;

}

Теперь рассмотрим использование указателей для массива указателей.

float **a;

Это указатель на float *,  или указатель на массив.

void main()

{

int n,m;

float **a;

a=new float *[n];

Создали массив указателей в количестве n штук на float, каждый элемент массива, является адресом, в котором хранится указатель на float. Осталось определить значение этого указателя. Для этого организуем цикл от 0 до n-1, в котором каждый указатель будет адресовать участок памяти, в котором хранится m элементов.

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

a[i]=new float(m);

ai,ja[i][j]

По окончании работы необходимо освободить память

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

delete a[i];

delete [];

Рассмотрим работу с динамической матрицей на примере

 

ЗАДАЧА 2. Задана матрица A(n,m). Сформировать вектор P(m), в который записать номера строк максимальных элементов каждого столбца. Алгоритм решения этой задачи следующий: для каждого столбца матрицы находим максимальный элемент и его номер, номер максимального элемента j-–го столбца матрицы записываем в j-–й элемент массива P.

 

#include "stdafx.h"

#include <iostream.h>

int main(int argc, char* argv[])

{

float max;

//float **a;

int *p;

int i,j,n,m,nmax;

cout<<"n=";

cin>>n;

cout<<"m=";

cin>>m;

cout<<"Vvod matrici"<<endl;

float **a=new float *[n];

//a==new float *[n];

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

a[i]=new float(m);

p=new int[m];

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

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

cin>>a[i][j];

cout<<"Matrica"<<endl;

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

{

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

cout<<a[i][j]<<"\t";

cout<<endl;

}

cout<<"Massiv P"<<endl;

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

{

max=a[0][j];

nmax=0;

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

if (a[i][j]>max)

{

max=a[i][j];

nmax=i;

}

p[j]=nmax;

cout<<p[j]<<"\t";

}

cout<<endl;

delete [] a;

return 0;

}

 

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

ЗАДАЧА 3. Проверить является ли матрица Х решением матричного уравнения A.B.X=C.D, где A(N,N), B(N,N), C(N,N), X(N,N), D(N,N) – квадратные матрицы.

Необходимо вычислить AB=A.B, Z=AB.X, CD=C.D, затем сравнить матрицы Z и CD. Если они совпадают, то X – решение матричного уравнения.

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

Рассмотрим процесс умножения двух матриц A(N,M)*B(M,L).

Воспользовавшись правилом «строка на столбец», получим матрицу:

В общем виде формула для нахождения элемента Cij матрицы имеет вид: , где i = 1,N и j = 1,L.

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

 

#include "stdafx.h"

#include <iostream.h>

void umn_matr(float **a, float **b, float **c,

 int n, int m, int l)

{

int i,j,k,s;

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

for(j=0;j<l;c[i][j]=s,j++)

for(s=0,k=0;k<m;k++)

s+=a[i][k]*b[k][j];

}

 

int main()

{

float **a,**b,**c,**d, **ab, **z, **cd, **x, eps=0.1;

int  i,j,n;

bool pr;

cout<<"n=";

cin>>n;

a=new float*[n];

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

a[i]=new float[n];

b=new float*[n];

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

b[i]=new float[n];

c=new float*[n];

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

c[i]=new float[n];

d=new float*[n];

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

d[i]=new float[n];

ab=new float*[n];

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

ab[i]=new float[n];

cd=new float*[n];

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

cd[i]=new float[n];

z=new float*[n];

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

z[i]=new float[n];

x=new float*[n];

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

x[i]=new float[n];

cout<<"Vvod matrici A\n";

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

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

cin>>a[i][j];

cout<<"Vvod matrici B\n";

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

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

cin>>b[i][j];

cout<<"Vvod matrici C\n";

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

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

cin>>c[i][j];

cout<<"Vvod matrici D\n";

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

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

cin>>d[i][j];

cout<<"Vvod matrici X\n";

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

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

cin>>x[i][j];

umn_matr(a,b,ab,n,n,n);

cout<<"Matrica AB"<<endl;

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

{

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

cout<<ab[i][j]<<"\t";

cout<<endl;

}

umn_matr(ab,x,z,n,n,n);

cout<<"Matrica Z"<<endl;

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

{

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

cout<<z[i][j]<<"\t";

cout<<endl;

}

umn_matr(c,d,cd,n,n,n);

cout<<"Matrica CD"<<endl;

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

{

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

cout<<cd[i][j]<<"\t";

cout<<endl;

}

pr=true;

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

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

//if (z[i][j]!=cd[i][j]) pr=false;

if ((z[i][j]-cd[i][j])<eps) pr=false;

if (pr)

cout<<"X-reshenie"<<endl;

else

cout<<"X- nereshenie"<<endl;

return 0;

}

 

Алгоритмы решения некоторых задач обработки матриц.

 

ЗАДАЧА 4. Проверить, является ли заданная квадратная матрица единичной.

Единичной называют матрицу, у которой элементы главной диагонали – единицы, а все остальные – нули. Например,

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

 

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

Задача 6. Преобразовать матрицу A(m,n) таким образом, чтобы каждый столбец был упорядочен по убыванию. Алгоритм решения этой задачи сводится к тому, что уже известный нам по предыдущей главе алгоритм упорядочивания элементов в массиве выполняется для каждого столбца матрицы.

ЗАДАЧА 7. Преобразовать матрицу A(m,n) так, чтобы строки с нечетными индексами были упорядочены по убыванию, c четными – по возрастанию.

 

 

Задача 8. Поменять местами n-й и l-й столбцы матрицы A(k,m).

Строки

Строка – последовательность символов. Если в выражении встречается одиночный символ, он должен быть заключен в одинарные кавычки. При использовании в выражениях строка заключается в двойные кавычки. Признаком конца строки является нулевой символ ‘\0’.  Строки в Си можно описать, как массив символов (массив элементов типа char). Объявляя такой массив, следует предусмотреть место для хранения признака конца строки (‘\0’).

Например, описание строки из 25 символов должно выглядеть так:

char s[26];

Здесь элемент s[26] предназначен для хранения символа конца строки.

 

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

1.                 Как осуществляется объявление двумерного массива (матрицы)?

2.                 Укажите блок-схемы ввода и вывода матриц.

3.                 Перечислите основные свойства матриц.

4.                 Охарактеризуйте алгоритм поиска максимального элемента и его индексов.

5.                 Охарактеризуйте алгоритм 1-го способа работы с динамическими матрицами.

6.                 Охарактеризуйте алгоритм 2-го способа работы с динамическими матрицами.

7.                 Укажите алгоритм проверки, является ли заданная квадратная матрица единичной.

8.                 Как описываются строки в массиве?

 

 

Глава 9. Объектно-ориентированное

программирование (ООП). Классы в С++

Объе́ктно-ориенти́рованное программи́рование (ООП) - парадигма программирования, в которой основными концепциями являются понятия объектов и классов (либо, в менее известном варианте языков с прототипированием, - прототипов).

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

Класс является описываемой на языке терминологии (пространства имён) исходного кода моделью ещё не существующей сущности, т.н. объекта.

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

Прототип - это объект-образец, по образу и подобию которого создаются другие объекты.

9.1 История развития

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

Первым языком программирования, в котором были предложены принципы объектной ориентированности, была Симула. В момент своего появления (в 1967 году), этот язык программирования предложил поистине революционные идеи: объекты, классы, виртуальные методы и др., однако это всё не было воспринято современниками как нечто грандиозное. Тем не менее, большинство концепций были развиты Аланом Кэйем и Дэном Ингаллсом в языке Smalltalk. Именно он стал первым широко распространённым объектно-ориентированным языком программирования.

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

 

9.2.Главные понятия и разновидности

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

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

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

Язык Self, соблюдая многие исходные положения объектно-ориентированного программирования, ввёл альтернативное классам понятие прототипа, положив начало прототипному программированию, считающемуся подвидом объектного.

Абстракция данных 

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

Инкапсуляция 

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

Сокрытие данных

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

Наследование

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

Полиморфизм

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

 

9.3. Определение ООП и его основные концепции

Сложности определения

ООП имеет уже более чем сорокалетнюю историю, но, несмотря на это, до сих пор не существует чёткого общепринятого определения данной технологии. Основные принципы, заложенные в первые объектные языки и системы, подверглись существенному изменению (или искажению) и дополнению при многочисленных реализациях последующего времени. Кроме того, примерно с середины 1980-х годов термин «объектно-ориентированный» стал модным, в результате с ним произошло то же самое, что несколько раньше с термином «структурный» (ставшим модным после распространения технологии структурного программирования) - его стали искусственно «прикреплять» к любым новым разработкам, чтобы обеспечить им привлекательность. Бьёрн Страуструп в 1988 году писал, что обоснование «объектной ориентированности» чего-либо, в большинстве случаев, сводится к силлогизму: «X - это хорошо. Объектная ориентированность - это хорошо. Следовательно, X является объектно-ориентированным».

Тимоти Бадд пишет:

Роджер Кинг аргументированно настаивал, что его кот является объектно-ориентированным. Кроме прочих своих достоинств, кот демонстрирует характерное поведение, реагирует на сообщения, наделён унаследованными реакциями и управляет своим, вполне независимым, внутренним состоянием.

Определение ООП

По мнению Алана Кея, создателя языка Smalltalk, которого считают одним из «отцов-основателей» ООП, объектно-ориентированный подход заключается в следующем наборе основных принципов (цитируется по вышеупомянутой книге Т. Бадда).

Всё является объектом.

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

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

3.                 Каждый объект является представителем (экземпляром) класса, который выражает общие свойства объектов.

4.                 В классе задаётся поведение (функциональность) объекта. Тем самым все объекты, которые являются экземплярами одного класса, могут выполнять одни и те же действия.

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

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

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

Концепции

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

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

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

Однако общность механизма обмена сообщениями имеет и другую сторону - «полноценная» передача сообщений требует дополнительных накладных расходов, что не всегда приемлемо. Поэтому в большинстве ныне существующих объектно-ориентированных языков программирования используется концепция «отправка сообщения как вызов метода» - объекты имеют доступные извне методы, вызовами которых и обеспечивается взаимодействие объектов. Данный подход реализован в огромном количестве языков программирования, в том числе C++, Object Pascal, Java, Oberon-2. В настоящий момент именно он является наиболее распространённым в объектно-ориентированных языках.

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

Особенности реализации

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

Поля данных

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

 

Методы 

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

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

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

Инкапсуляция обеспечивается следующими средствами

·                   Контроль доступа 

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

·                   Методы доступа 

Поля класса, в общем случае, не должны быть доступны извне, поскольку такой доступ позволил бы произвольным образом менять внутреннее состояние объектов. Поэтому поля обычно объявляются скрытыми (либо язык в принципе не позволяет обращаться к полям класса извне), а для доступа к находящимся в полях данным используются специальные методы, называемые методами доступа. Такие методы либо возвращают значение того или иного поля, либо производят запись в это поле нового значения. При записи метод доступа может проконтролировать допустимость записываемого значения и, при необходимости, произвести другие манипуляции с данными объекта, чтобы они остались корректными (внутренне согласованными). Методы доступа называют ещё аксессорами (от англ. access - доступ), а по отдельности - геттерами (англ. get - чтение) и сеттерами (англ. set - запись).

·                   Свойства объекта 

Псевдополя, доступные для чтения и/или записи. Свойства внешне выглядят как поля и используются аналогично доступным полям (с некоторыми исключениями), однако фактически при обращении к ним происходит вызов методов доступа. Таким образом, свойства можно рассматривать как «умные» поля данных, сопровождающие доступ к внутренним данным объекта какими-либо дополнительными действиями (например, когда изменение координаты объекта сопровождается его перерисовкой на новом месте). Свойства, по сути - не более чем синтаксический сахар, поскольку никаких новых возможностей они не добавляют, а лишь скрывают вызов методов доступа. Конкретная языковая реализация свойств может быть разной. Например, в C# объявление свойства непосредственно содержит код методов доступа, который вызывается только при работе со свойствами, то есть не требует отдельных методов доступа, доступных для непосредственного вызова. В Delphi объявление свойства содержит лишь имена методов доступа, которые должны вызываться при обращении к полю. Сами методы доступа представляют собой обычные методы с некоторыми дополнительными требованиями к сигнатуре.

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

 

9.4. Подходы к проектированию программ в целом

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

Объектно-ориентированное проектирование основывается на описании структуры и поведения проектируемой системы, то есть, фактически, в ответе на два основных вопроса:

·                   Из каких частей состоит система.

·                   В чём состоит ответственность каждой из частей.

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

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

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

 

Родственные методологии

·                   Компонентное программирование

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

·                   Прототипное программирование

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

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

-Каждый вновь созданный объект является «экземпляром без класса». Каждый объект может стать прототипом - быть использован для создания нового объекта с помощью операции клонирования. После клонирования новый объект может быть изменён, в частности, дополнен новыми полями и методами.

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

 

Производительность объектных программ

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

Динамическое связывание методов.

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

Значительная глубина абстракции.

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

Наследование «размывает» код.

Код, относящийся к «оконечным» классам иерархии наследования (которые обычно и используются программой непосредственно) - находится не только в самих этих классах, но и в их классах-предках. Относящиеся к одному классу методы фактически описываются в разных классах. Это приводит к двум неприятным моментам:

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

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

Инкапсуляция снижает скорость доступа к данным.

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

Динамическое создание и уничтожение объектов.

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

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

 

9.5 Критика ООП

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

Обычно сравнивают объектное и процедурное программирование:

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

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

Критические высказывания в адрес ООП:

·                   Исследование Thomas E. Potok, Mladen Vouk и Andy Rindos [1] показало отсутствие значимой разницы в продуктивности разработки программного обеспечения между ООП и процедурным подходом.

·                   Кристофер Дэйт указывает на невозможность сравнения ООП и других технологий во многом из-за отсутствия строгого и общепризнанного определения ООП (C. J. Date, Introduction to Database Systems, 6th-ed., Page 650)

·                   Александр Степанов, в одном из своих интервью, указывал на то, что ООП «методологически неправильно» и что «… ООП практически такая же мистификация как и искусственный интеллект…» ([2]).

·                   Фредерик Брукс (Frederick P. Brooks, Jr.) в своей статье «No Silver Bullet. Essence and Accidents of Software Engineering» (Computer Magazine; April 1987) указывает на то, что наиболее сложной частью создания программного обеспечения является « … спецификация, дизайн и тестирование концептуальных конструкций, а отнюдь не работа по выражению этих концептуальных конструкций…». ООП (наряду с такими технологиями как искусственный интеллект, верификация программ, автоматическое программирование, графическое программирование, экспертные системы и др.), по его мнению, не является «серебряной пулей», которая могла бы на порядок величины (т.е. примерно в 10 раз, как говорится в статье) снизить сложность разработки программных систем. Согласно Бруксу, «…ООП позволяет сократить только привнесённую сложность в выражение дизайна. Дизайн остаётся сложным по своей природе…». ([3])

·                   Эдсгер Дейкстра указывал: «… то о чём общество в большинстве случаев просит - это змеиное масло. Естественно, „змеиное масло“ имеет очень впечатляющие имена, иначе будет очень трудно что-то продать: „Структурный анализ и Дизайн“, „Программная инженерия“, „Модели зрелости“, „Управляющие информационные системы“ (Management Information Systems), „Интегрированные среды поддержки проектов“, „Объектная ориентированность“, „Реинжиниринг бизнес-процессов“…» - EWD 1175: The strengths of the academic enterprise

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

·                   Патрик Киллелиа в своей книге «Тюнинг веб-сервера» писал: «… ООП предоставляет вам множество способов замедлить работу ваших программ …»

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

Критика рекламы ООП.

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

Оспаривание эффективности разработки методами ООП.

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

Производительность объектно-ориентированных программ.

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

Критика отдельных технологических решений в ООП-языках и библиотеках.

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

 

9.6.Объектно-ориентированные языки

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

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

·                   Объявление классов с полями (данными - членами класса) и методами (функциями - членами класса).

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

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

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

·                   Полиморфное поведение экземпляров классов за счёт использования виртуальных методов. В некоторых ООП-языках все методы классов являются виртуальными.

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

·                   Конструкторы, деструкторы, финализаторы.

·                   Свойства (аксессоры).

·                   Индексаторы.

·                   Интерфейсы - как альтернатива множественному наследованию.

·                   Переопределение операторов для классов.

Часть языков (иногда называемых «чисто объектными») целиком построена вокруг объектных средств - в них любые данные (возможно, за небольшим числом исключений в виде встроенных скалярных типов данных) являются объектами, любой код - методом какого-либо класса, и невозможно написать программу, в которой не использовались бы объекты. Примеры подобных языков - C#, Smalltalk, Java, Ruby. Другие языки (иногда используется термин «гибридные») включают ООП-подсистему в исходно процедурный язык. В них существует возможность программировать, не обращаясь к объектным средствам. Классические примеры - C++ и Pascal.

 

1. Общие сведения о классах

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

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

Объектно-ориентированное программирование (ООП) - представляет собой технологию разработки программ с использованием объектов.

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

Рассмотрим пример определения класса complex.

class complex {

public:

    float x;

    float y;

    float modul( ) {return pow(x*x+y*y,0.5);}

    float argument() {return pow(x*x+y*y,0.5);}

    void show_complex()

}

и пример его использования

Листинг 1.

#include <iostream.h>

#include <math.h>

#define PI 3.14159

class complex {

public:

    float x;

    float y;

    float modul() {return pow(x*x+y*y,0.5);}

    float argument() {return atan2(y,x)*180/PI;}

    void show_complex() {if (y>=0) cout<<x<<"+"<<y<<"i"<<endl;

             else cout<<x<<y<<"i"<<endl;}

};

int main()

 

{

// Объявление переменной класса chislo

  complex chislo;  chislo.x=3.5;  chislo.y=-1.432;

  chislo.show_complex();

  cout<<"Модуль числа"<<chislo.modul();

  cout<<endl<<"Аргумент числа"<<chislo.argument()<<endl;       

   return EXIT_SUCCESS;

}

Если поля структуры доступны всегда, то при использовании классов могут быть члены, доступные (публичные) (описатель public), с помощью оператора . ("точка") и приватные (описатель private), доступ к которым возможен только с помощью публичных методов. Методы также могут быть публичными и приватными. Вызов приватных методов осуществляется из публичных.

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

Рассмотрим пример работы с классом complex, в котором члены являются приватными.

Листинг 2.
#include "stdafx.h"
#include <iostream.h>
#include <math.h>
#define PI 3.14159
class complex {
//Публичные  методы
public:
     void znach();
     float modul();
     float argument();
//Приватные члены и методы
private:
     float x;
     float y;
     void show_complex();
 };
void complex::znach()
{
cout<<"VVedite x\t";
cin>>x;
cout<<"Vvedite y\t";
cin>>y;
// Вызов приватного метода
show_complex();
}
float complex::modul()
{
return pow(x*x+y*y,0.5);
}
float complex::argument()
{
return atan2(y,x)*180/PI;
}
void complex::show_complex()
{
if (y>=0) cout<<x<<"+"<<y<<"i"<<endl;
              else cout<<x<<y<<"i"<<endl;
}
int main(int argc, char *argv[])
{
  complex chislo;
  chislo.znach();
  cout<<"Modul kompleksnogo chisla="<<chislo.modul();
  cout<<endl<<"Argument kompleksnogo chisla="<<chislo.argument()<<endl;
  return 1;
}

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

VVedite x       3
Vvedite y       -1
3-1i
Modul kompleksnogo chisla=3.16228
Argument kompleksnogo chisla=-18.435
Press any key to continue

2. Функция-конструктор

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

На листинге 3 приведен класс complex с использованием конструктора.

Листинг 3.
#include "stdafx.h"
#include <iostream.h>
#include <math.h>
#define PI 3.14159
class complex {
 public:
     complex();
     float modul();
     float argument();
private:
     float x;
     float y;
     void show_complex();
 };
int main(int argc, char *argv[])
{
  complex chislo;
  cout<<"Modul kompleksnogo chisla="<<chislo.modul();
  cout<<endl<<"Argument kompleksnogo chisla="<<chislo.argument()<<endl;
  return 1;
}
complex::complex()
{
cout<<"VVedite x\t";
cin>>x;
cout<<"Vvedite y\t";
cin>>y;
show_complex();
}
float complex::modul()
{
return pow(x*x+y*y,0.5);
}
float complex::argument()
{
return atan2(y,x)*180/PI;
}
void complex::show_complex()
{
if (y>=0) cout<<x<<"+"<<y<<"i"<<endl;
              else cout<<x<<y<<"i"<<endl;
}

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

ЗАДАЧА. Заданы координаты n точек в к-мерном пространстве. Найти точки, расстояние между которыми наибольшее и наименьшее.
Листинг
4.
#include "stdafx.h"
#include <iostream.h>
#include <math.h>
class prostr{
public:
    prostr();
    float poisk_min();
    float poisk_max();
    int vivod_result();
    int delete_a();
private:
    int n;
    int k;
    float **a;
    float min;
    float max;
    int imin;
    int jmin;
    int imax;
    int jmax;
};
void  main()
{
    prostr x;
    x.poisk_max();
    x.poisk_min();
    x.vivod_result();
    x.delete_a();
}
prostr::prostr()
{
    int i,j;
    cout<<"VVedite razmernost prostrantva ";
    cin>>k;
    cout<<"VVedite kolichestvo tochek ";
    cin>>n;
    a=new float*[k];
    for(i=0;i<k;i++)
        a[i]=new float[n];
    for(j=0;j<n;j++)
    {
    cout<<"VVedite koordinati "<<j<<" tochki"<<endl;
    for(i=0;i<k;i++)
        cin>>a[i][j];
    }

}
float prostr::poisk_max()
{
    int i,j,l;
    float s;
    for(max=0,l=0;l<k;l++)
                max+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);
    max=pow(max,0.5);
    imax=0;jmax=1;
    for(i=0;i<n;i++)
        for(j=i+1;j<n;j++)
        {
            for(s=0,l=0;l<k;l++)
                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);
            s=pow(s,0.5);
            if (s>max)
            {
                max=s;
                imax=i;
                jmax=j;
            }
        }
        return 0;
}
float prostr::poisk_min()
{
    int i,j,l;
    float s;
    for(min=0,l=0;l<k;l++)
                min+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);
    min=pow(min,0.5);
    imin=0;jmin=1;
    for(i=0;i<k;i++)
        for(j=i+1;j<n;j++)
        {
            for(s=0,l=0;l<k;l++)
                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);
            s=pow(s,0.5);
            if (s<min)
            {
                min=s;
                imin=i;
                jmin=j;
            }
        }
    return 0;
}   
int prostr::vivod_result()
{
    int i,j;
    for(i=0;i<k;cout<<endl,i++)
    for (j=0;j<n;j++)
        cout<<a[i][j]<<"\t";
    cout<<"max="<<max<<"\t nomera "<<imax<<"\t"<<jmax<<endl;
    cout<<"min="<<min<<"\t nomera "<<imin<<"\t"<<jmin<<endl;
    return 0;
}
int prostr::delete_a()
{
    delete [] a;
    return 0;
}

Перегрузка функций-конструкторов, параметры по умолчанию в конструкторе

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

#include "stdafx.h"

#include <iostream.h>

#include <math.h>

class prostr{

public:

    prostr(int,int);

    prostr();

    float poisk_min();

    float poisk_max();

    int vivod_result();

    int delete_a();

private:

    int n;

    int k;

    float **a;

    float min;

    float max;

    int imin;

    int jmin;

    int imax;

    int jmax;

};

void  main()

{

    prostr x(3,5);

//  prostr x;

    x.poisk_max();

    x.poisk_min();

    x.vivod_result();

    x.delete_a();

}

prostr::prostr()

{

    int i,j;

    cout<<"VVedite razmernost prostrantva ";

    cin>>k;

    cout<<"VVedite kolichestvo tochek ";

    cin>>n;

    a=new float*[k];

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

        a[i]=new float[n];

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

    {

    cout<<"VVedite koordinati "<<j<<" tochki"<<endl;

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

        cin>>a[i][j];

    }

}

prostr::prostr(int k1, int n1)

{

    int i,j;

    k=k1;

    n=n1;

        a=new float*[k];

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

        a[i]=new float[n];

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

    {

    cout<<"VVedite koordinati "<<j<<" tochki"<<endl;

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

        cin>>a[i][j];

    }

}

float prostr::poisk_max()

{

    int i,j,l;

    float s;

    for(max=0,l=0;l<k;l++)

                max+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);

    max=pow(max,0.5);

    imax=0;jmax=1;

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

        for(j=i+1;j<n;j++)

        {

            for(s=0,l=0;l<k;l++)

                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);

            s=pow(s,0.5);

            if (s>max)

            {

                max=s;

                imax=i;

                jmax=j;

            }

        }

        return 0;

}
float prostr::poisk_min()

{
    int i,j,l;

    float s;

    for(min=0,l=0;l<k;l++)

                min+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);

    min=pow(min,0.5);

    imin=0;jmin=1;

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

        for(j=i+1;j<n;j++)

        {

            for(s=0,l=0;l<k;l++)

                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);

            s=pow(s,0.5);

            if (s<min)

            {

                min=s;

                imin=i;

                jmin=j;

            }

        }

    return 0;

}   
int prostr::vivod_result()

{
    int i,j;

    for(i=0;i<k;cout<<endl,i++)

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

        cout<<a[i][j]<<"\t";

    cout<<"max="<<max<<"\t nomera "<<imax<<"\t"<<jmax<<endl;

    cout<<"min="<<min<<"\t nomera "<<imin<<"\t"<<jmin<<endl;

    return 0;

}
int prostr::delete_a()

{
    delete [] a;

    return 0;}

Теперь рассмотрим как можно использовать параметры по умолчанию в конструкторе. Оставим в задаче только конструктор с параметрами (n и k), но сделаем их по умолчанию равными 2 и 10 соответственно.

#include "stdafx.h"

#include <iostream.h>

#include <math.h>

class prostr{

public:
    prostr(int k1=2,int n1=10);

    //prostr();

    float poisk_min();

    float poisk_max();

    int vivod_result();

    int delete_a();

private:
    int n;

    int k;

    float **a;

    float min;

    float max;

    int imin;

    int jmin;

    int imax;

    int jmax;

};

void  main()

{

    prostr x(2,3);

    //prostr x;

    prost x;

    x.poisk_max();

    x.poisk_min();

    x.vivod_result();

    x.delete_a();

}

prostr::prostr(int k1, int n1)

{    int i,j;

    k=k1;

    n=n1;

        a=new float*[k];

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

        a[i]=new float[n];

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

    {

    cout<<"VVedite koordinati "<<j<<" tochki"<<endl;

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

        cin>>a[i][j];

    }

}

float prostr::poisk_max()

{

    int i,j,l;

    float s;

    for(max=0,l=0;l<k;l++)

                max+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);

    max=pow(max,0.5);

    imax=0;jmax=1;

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

        for(j=i+1;j<n;j++)

        {

            for(s=0,l=0;l<k;l++)

                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);

            s=pow(s,0.5);

            if (s>max)

            {

                max=s;

                imax=i;

                jmax=j;

            }

        }

        return 0;

}

float prostr::poisk_min()

{

    int i,j,l;

    float s;

    for(min=0,l=0;l<k;l++)

                min+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);

    min=pow(min,0.5);

    imin=0;jmin=1;

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

        for(j=i+1;j<n;j++)

        {

            for(s=0,l=0;l<k;l++)

                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);

            s=pow(s,0.5);

            if (s<min)

            {

                min=s;

                imin=i;

                jmin=j;

            }

        }

    return 0;

}   

int prostr::vivod_result()

{

    int i,j;

    for(i=0;i<k;cout<<endl,i++)

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

        cout<<a[i][j]<<"\t";

    cout<<"max="<<max<<"\t nomera "<<imax<<"\t"<<jmax<<endl;

    cout<<"min="<<min<<"\t nomera "<<imin<<"\t"<<jmin<<endl;

    return 0;

}
int prostr::delete_a()

{
    delete [] a;

    return 0;

}

 

3. Функция-деструктор

С++ позволяет построить функцию-деструктор, которая выполняется при уничтожении экземпляра класса, что происходит в следующих случаях:

1.                 При завершении программы  или выходе из функции.

2.                 При освобождении  памяти, выделенной  для экземпляра класса.

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

#include "stdafx.h"

#include <iostream.h>

#include <math.h>

class prostr{

public:
    prostr(int k1=2,int n1=10);

    ~prostr();

    //prostr();

    float poisk_min();

    float poisk_max();

    int vivod_result();

    int delete_a();

private:
    int n;

    int k;

    float **a;

    float min;

    float max;

    int imin;

    int jmin;

    int imax;

    int jmax;

};
void  main()

{
    prostr x(2,3);

    x.poisk_max();

    x.poisk_min();

    x.vivod_result();

    x.delete_a();

}

prostr::prostr(int k1, int n1)

{
    int i,j;

    k=k1;

    n=n1;

        a=new float*[k];

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

        a[i]=new float[n];

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

    {

    cout<<"VVedite koordinati "<<j<<" tochki"<<endl;

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

        cin>>a[i][j];

    }

}
prostr::~prostr()
{
    cout<<"delete object!!!"<<endl;

}
float prostr::poisk_max()

{
    int i,j,l;    float s;

    for(max=0,l=0;l<k;l++)

                max+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);

    max=pow(max,0.5);

    imax=0;jmax=1;

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

        for(j=i+1;j<n;j++)

        {

            for(s=0,l=0;l<k;l++)

                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);

            s=pow(s,0.5);

            if (s>max)

            {

                max=s;

                imax=i;

                jmax=j;

            }

        }

        return 0;

}
float prostr::poisk_min()

{
    int i,j,l;

    float s;

    for(min=0,l=0;l<k;l++)

                min+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);

  min=pow(min,0.5);

    imin=0;jmin=1;

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

        for(j=i+1;j<n;j++)

        {

            for(s=0,l=0;l<k;l++)


                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);

            s=pow(s,0.5);

            if (s<min)

            {

                min=s;

                imin=i;

                jmin=j;

            }

        }

    return 0;

}   
int prostr::vivod_result()

{

    int i,j;

    for(i=0;i<k;cout<<endl,i++)

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

        cout<<a[i][j]<<"\t";

    cout<<"max="<<max<<"\t nomera "<<imax<<"\t"<<jmax<<endl;

    cout<<"min="<<min<<"\t nomera "<<imin<<"\t"<<jmin<<endl;

    return 0;

}

int prostr::delete_a()

{

    delete [] a;

    return 0;

}

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

VVedite koordinati 0 tochki

2 3

VVedite koordinati 1 tochki

4 5

VVedite koordinati 2 tochki

6 7

2       4       6

3       5       7

max=5.65685      nomera 0       2

min=2.82843      nomera 0       1

delete object!!!

Press any key to continue

 

4. Массив объектов класса

Для массива пространств вернемся к конструктору, который определяет значения n и k по вводимым с экрана числам. Ниже приведен пример массива из элементов.

#include "stdafx.h"
#include <iostream.h>
#include <math.h>
class prostr{
public:
    prostr();
    ~prostr();
    float poisk_min();
    float poisk_max();
    int vivod_result();
    int delete_a();
private:
    int n;
    int k;
    float **a;
    float min;
    float max;
    int imin;
    int jmin;
    int imax;
    int jmax;
};
void  main()
{
    int m;
    prostr x[3];
    for(m=0;m<3;m++)
    {
    x[m].poisk_max();
    x[m].poisk_min();
    x[m].vivod_result();
    x[m].delete_a();
    }
}
prostr::prostr()
{
    int i,j;
    cout<<"VVedite razmernost prostrantva ";
    cin>>k;
    cout<<"VVedite kolichestvo tochek ";
    cin>>n;
    a=new float*[k];
    for(i=0;i<k;i++)
        a[i]=new float[n];
    for(j=0;j<n;j++)
    {
    cout<<"VVedite koordinati "<<j<<" tochki"<<endl;
    for(i=0;i<k;i++)
        cin>>a[i][j];
    }

}
prostr::~prostr()
{
    cout<<"delete object!!!"<<endl;
}
float prostr::poisk_max()
{
    int i,j,l;
    float s;
    for(max=0,l=0;l<k;l++)
                max+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);
    max=pow(max,0.5);
    imax=0;jmax=1;
    for(i=0;i<n;i++)
        for(j=i+1;j<n;j++)
        {
            for(s=0,l=0;l<k;l++)
                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);
            s=pow(s,0.5);
            if (s>max)
            {
                max=s;
                imax=i;
                jmax=j;
            }
        }
        return 0;
}
float prostr::poisk_min()
{
    int i,j,l;
    float s;
    for(min=0,l=0;l<k;l++)
                min+=(a[l][0]-a[l][1])*(a[l][0]-a[l][1]);
    min=pow(min,0.5);
    imin=0;jmin=1;
    for(i=0;i<k;i++)
        for(j=i+1;j<n;j++)
        {
            for(s=0,l=0;l<k;l++)
                s+=(a[l][i]-a[l][j])*(a[l][i]-a[l][j]);
            s=pow(s,0.5);
            if (s<min)
            {
                min=s;
                imin=i;
                jmin=j;
            }
        }
    return 0;
}   
int prostr::vivod_result()
{
    int i,j;
    for(i=0;i<k;cout<<endl,i++)
    for (j=0;j<n;j++)
        cout<<a[i][j]<<"\t";
    cout<<"max="<<max<<"\t nomera "<<imax<<"\t"<<jmax<<endl;
    cout<<"min="<<min<<"\t nomera "<<imin<<"\t"<<jmin<<endl;
    return 0;
}
int prostr::delete_a()
{
    delete [] a;
    return 0;
}

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

VVedite razmernost prostrantva 2

VVedite kolichestvo tochek 3
VVedite koordinati 0 tochki
0 0
VVedite koordinati 1 tochki
1 1
VVedite koordinati 2 tochki
2 2
VVedite razmernost prostrantva 3 4
VVedite kolichestvo tochek VVedite koordinati 0 tochk
0 0 0
VVedite koordinati 1 tochki
1 1 1
VVedite koordinati 2 tochki
2 2 2
VVedite koordinati 3 tochki
3 3 3
VVedite razmernost prostrantva 2
VVedite kolichestvo tochek 2
VVedite koordinati 0 tochki
0 0
VVedite koordinati 1 tochki
1 1
0
       1       2
0
       1       2
max=2.82843      nomera 0       2
min=1.41421      nomera 0       1
0
       1       2       3
0
       1       2       3
0
       1       2       3
max=5.19615      nomera 0       3
min=1.73205      nomera 0       1
0
       1
0
       1
max=1.41421      nomera 0       1
min=1.41421      nomera 0       1
delete object!!!
delete object!!!
delete object!!!
Press any key to continue

 

5. Динамический массив объектов класса

Для создания динамического массива объектов несколько изменим главную функцию.

void  main()
{
    int m,m1;
    cout<<"m1=";cin>>m1;
    prostr *x;
    x=new prostr[m1];
    for(m=0;m<m1;m++)
    {
    x[m].poisk_max();
    x[m].poisk_min();
    x[m].vivod_result();
    x[m].delete_a();
    }
}

6. Указатель this

При создании в программе переменной(объекта) типа class, формируется указатель this, в котором хранится адрес этой переменной. Указатель this выступает в виде указателя на текущий объект.

 

7. Перегрузка операций (операторов)

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

1.                 Нельзя изменить приоритет операторов.

2.                 Нельзя изменить тип оператора из унарного оператора нельзя сделать бинарный и наоборот.

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

4.                 Нельзя создавать новые операторы.

5.                 Запрещено перегружать операторы :  .(доступ к членам класса), *(значение по адресу указателя), ::(расширение области видимости)   , ?: (операция if).

6.                 Допустима перегрузка следующих операторов: +, -, *, /, %, =, <, >, +=, -=, *=, /=, <, >, &&, ||, ++, --, (), [], new, delete.

Для перегрузке бинарного оператора внутри класса должна быть строка
type operator symbols(type1 parametr)
здесь
type - тип возвращаемого оператором значения,
operator - служебное слово,
symbols - перегружаемый оператор,
type - тип второго операнда, первым операндом является  экземпляр  класса.
parametr - имя переменной второго операнда.
В качестве примера рассмотрим класс complex с переопределенными операндами +(сложение комплексных чисел), -(вычитание комплексных чисел).
#include <iostream.h>
class complex {
 public:
     complex(bool pr=true);
     complex operator+(complex M);
     complex operator-(complex M)
     {
         complex temp(false);
         temp.x=x-M.x;
         temp.y=y-M.y;
         return temp;
     };   
     float x;
     float y;
     void show_complex();
 };
int main()
{
  complex chislo1, chislo2, chislo4(false), chislo3(false);
  chislo3=chislo1+chislo2;
  cout<<"chislo3=";
  chislo3.show_complex();
  chislo4=chislo1-chislo2;
  cout<<"chislo4=";
  chislo4.show_complex();
 
  return 1;
}
complex::complex(bool pr)
{
    if (pr)
    {
        cout<<"VVedite x\t";
        cin>>x;
        cout<<"Vvedite y\t";
        cin>>y;
        show_complex();
    }
    else{x=0;y=0;}
}
void complex::show_complex()
{
if (y>=0) cout<<x<<"+"<<y<<"i"<<endl;
              else cout<<x<<y<<"i"<<endl;
}
complex complex::operator+(complex M)
{
    complex temp(false);
    temp.x=x+M.x;
    temp.y=y+M.y;
    //x=x+M.x;
    //y=y+M.y;
    return temp;
}

Рассмотрим пример перегрузки унарного оператора ++ для комплексных чисел,  пусть ++complex увеличивает мнимую часть числа, а complex++ увеличивает действительную часть числа.
#include "stdafx.h"
#include <iostream.h>
class complex {
 public:
     complex(bool pr=true);
     complex operator++()
     {
    //
Реализует ++x
         y++;
         return *this;
     }
     complex operator++(int)
     {
         //
Реализует x++
         x++;
         return *this;
     }
     float x;
     float y;
     void show_complex();
 };
int main()
{
  complex chislo2;
  ++chislo2;
  cout<<"++chislo2=";
  chislo2.show_complex();
  chislo2++;
  cout<<"chislo2++=";
  chislo2.show_complex();
    return 1;
}
complex::complex(bool pr)
{
    if (pr)
    {
        cout<<"VVedite x\t";
        cin>>x;
        cout<<"Vvedite y\t";
        cin>>y;
        show_complex();
    }
    else{x=0;y=0;}

}
void complex::show_complex()
{
if (y>=0) cout<<x<<"+"<<y<<"i"<<endl;
              else cout<<x<<y<<"i"<<endl;
}
Результаты работы программы
VVedite x       4
Vvedite y       5
4+5i
++chislo2=4+6i
chislo2++=5+6i
Press any key to continue

 

8. Статические члены классов и  статические методы

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

 

9. Наследование

Одной из основных особенностей ООП является наследование.

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

При использовании наследования члены и методы кроме свойств public и private могут иметь свойство protected. Для одиночного класса описатели protected и private равносильны. Разница между protected и private проявляется при наследовании, защищенные члены и методы, объявленные в базовом классе, как protected, в производном могут использоваться, как публичные (public). Защищенные(protected) члены и методы  являются чем-то промежуточным между public и private.

При создании производного класса используется следующий синтаксис.

class name_derived_class: type_inheritance base_class {

// приватные члены и методы класса

...
public:
// публичные члены и методы класса

...
protected:
// защищенные члены и методы класса

...
};
Здесь
name_derived_class - имя создаваемого производного класса,

type_inheritance - способ наследования, возможны следующие способы наследования public, private и protected.

base_class - имя базового типа.

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

Таблица

Способ

доступа

Спецификатор в базовом классе

Доступ в производном классе

private

private
protected
public

нет
private
private

protected

private
protected
public

нет
protected
protected

public

private
protected
public

нет
protected
public

Конструкторы производного типа

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

Опережающее описание класса

Если в описании класса есть ссылка на описываемый позже класс, то его надо просто объявить с помощью оператора class new_class;

Это описание аналогично описанию прототипов функции.

 

10. Виртуальные методы

Зачастую при наследовании появляются методы, которые в различных производных классах работают по различным алгоритмам, но имеют одинаковые выходные параметры и возвращаемое значение. Такие методы называются виртуальными и описываются с помощью служебного слова virtual. В качестве примера приведем абстрактный класс figure (фигура), на базе которого можно построить классы окружность, прямоугольник, квадрат, треугольник, ромб, квадрат, эллипс. На листинге приведены  классы окружность и прямоугольник.
#
include <iostream.h>

#include <math.h>

#define PI 3.14159

class figure

{
    public:

    int n;

    float *p;

    figure();

    float perimetr();


    virtual float square();

    virtual void show_parametri();

};
figure::figure()

{

    cout<<"This is abstract constructor"<<endl;

}

float figure::perimetr()

{
    int i;

    float psum;

    for(psum=0,i=0;i<n;psum+=p[i],i++);

    return psum;


}
float figure::square()

{
    cout<<"No square abstract figure"<<endl;

    return 0;

}
void figure::show_parametri()

{
    cout<<"Abstract figure";

}
class _circle:public figure

{
    public:

    _circle();    float perimetr();

    virtual float square();

    virtual void show_parametri();

};
class RecTangle:public figure

{
    public:

    RecTangle();

    virtual float square();

    virtual void show_parametri();

};
void main()

{
    _circle RR;

    RR.show_parametri();

    RecTangle PP;

    PP.show_parametri();

}
_circle::_circle()
{
    cout<<"Parametri okruzhnosti"<<endl;

    n=1;

    p=new float[n];

    cout<<"Vvedite radius";

    cin>>p[0];

}
float _circle::perimetr()

{
    return 2*PI*p[0];

}
float _circle::square()

{
    return PI*p[0]*p[0];

}
void _circle::show_parametri()

{
    cout<<"This is circle"<<endl;

    cout<<"Radius="<<p[0]<<endl;

    cout<<"Perimetr="<<perimetr()<<endl;

    cout<<"Square="<<square()<<endl;

}
RecTangle::RecTangle()
{
    cout<<"Parametri rectangle"<<endl;

    n=4;    p=new float[n];

    cout<<"Vvedite dlini storon";

    cin>>p[0]>>p[1];

    p[2]=p[0];

    p[3]=p[1];

}
float RecTangle::square()

{
    return p[0]*p[1];

}
void RecTangle::show_parametri()

{
    cout<<"This is Rectangle"<<endl;

    cout<<"a="<<p[0]<<" b="<<p[1]<<endl;

    cout<<"Perimetr="<<perimetr()<<endl;

    cout<<"Square="<<square()<<endl;

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

This is abstract constructor

Parametri okruzhnosti

Vvedite radius

10
This is circle

Radius=10
Perimetr=62.8318
Square=314.159
This is abstract constructor

Parametri rectangle

Vvedite dlini storon

10
20
This is Rectangle

a=10 b=20

Perimetr=60
Square=200
Press any key to continue

 

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

1.                 Что такое объе́ктно-ориенти́рованное программи́рование?

2.                 Что называется классом?

3.                 Что такое объект?

4.                 Что такое прототип?

5.                 Что называется абстракцией данных?

6.                 Что такое инкапсуляция?

7.                 В чем состоит процесс сокрытия данных?

8.                 Что такое наследование?

9.                 Укажите основные свойства наследования.

10.            Что такое полиморфизм?

11.            Укажите основные концепции ООП.

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

13.            В чем отличие компонентно-ориентированного программирования от прототипного?

14.            Укажите основные принципы производительности объектных программ.

15.            В чем суть работы функции-конструктор?

16.            Как осущетсвляется перегрузка функций-конструкторов?

17.            Как осуществляется построение функции-деструктор?

18.            Назначение указателя this.

19.            Создание статических членов классов и статических методов?

20.            В чем суть виртуальных методов?

 

Глава 10. Основные виды, этапы проектирования и жизненный цикл программных продуктов

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

Обратимся к стандарту ГОСТ Р ИСО/МЭК 12207 «Информационные технологии. Процессы жизненного цикла программных средств», т.к.:

1. Этот стандарт является российским, официально введенным в действие на территории Российской Федерации.

2. Он является переводом одного из наиболее популярных международных стандартов в сфере информационных технологий – ISO/IEC 12207:1995 (ISO/IEC12207) Standard for Information Technology - Software Lifecycle Processes, а популярные методологии разработки ПС (такие как Rational Unified Process) основываются на ISO/IEC 12207:1995 (ISO/IEC12207) Standard for Information Technology - Software Lifecycle Processes.

Российский стандарт ГОСТ Р ИСО/МЭК 12207 рассматривает процессы жизненного цикла (ЖЦ) программных средств (ПС) на основе трех групп:

1. Основные.

2. Вспомогательные.

3. Организационные.

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

 

Рис. 1. Процессы жизненного цикла ПС по ГОСТ Р ИСО/МЭК 12207.

Стандарт ГОСТ Р ИСО/МЭК 12207 устанавливает общую структуру процессов жизненного цикла (ЖЦ) программных средств (ПС), определяет процессы, работы и задачи, выполняемые в ходе ЖЦ ПС. Данный процесс предполагает выполнение следующих работ:

1. подготовка процесса;

2. определение конфигурации;

3. контроль конфигурации;

4. учет состояний конфигурации;

5. оценка конфигурации;

6. управление выпуском и поставка.

Подготовка процесса

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

- работы по управлению конфигурацией;

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

- организацию(и), ответственную(ые) за выполнение данных работ;

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

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

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

Определение конфигурации

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

Контроль конфигурации

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

 

Учет состояний конфигурации

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

 

 

Оценка конфигурации

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

Управление выпуском и поставка

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

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

1. Последовательный тип

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

- требования к продукту четко определены и не меняются со временем;

- имеется достаточно большой опыт реализации подобного рода систем;

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

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

2. Эволюционный тип

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

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

- сложность в управлении и контроле выполнения проекта;

- сложность оценки затрат на разработку;

- риск бесконечного улучшения системы.

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

Выбор типа жизненного цикла

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

- наличия четких и подробных требований к ПО;

- ресурсов, имеющихся в наличии для проведения работ по проекту;

- наличия и доступности заказчика в процессе разработки;

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

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

 

Унифицированный Процесс Разработки

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

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

 

Основные элементы Рационального

Унифицированного Процесса

 

Характеристики процесса

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

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

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

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

Рациональный Унифицированный Процесс поддерживает объектно-ориентированные методики. Каждая модель объектно-ориентированна. Модели, применяемые в рамках Рационального Унифицированного Процесса, основаны на понятиях объектов и классов и отношений между ними, а в качестве общей нотации используют нотацию UML

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

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

Фазы, итерации и циклы разработки

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

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

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

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

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

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

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

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

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

Рабочие процессы

Рациональный Унифицированный Процесс состоит из девяти рабочих процессов:

- моделирование процессов - описывается структура и динамика ПО в целом;

- разработка требований - описывается основанный на прецедентах метод постановки требований;

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

- реализация - собственно разработка программ, автономное тестирование и интеграция;

- тестирование - описываются тестовые сценарии, процедуры и метрики для измерения числа ошибок;

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

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

- управление проектом - описывает разные стратегии работы с итеративным процессом;

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

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

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

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

Артефакты

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

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

Модели

Модели - это самый важный вид артефактов в Рациональном Унифицированном Процессе.

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

- модель процессов - формализует абстракцию ПО в целом;

- модель предметной области - формализует контекст системы;

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

- аналитическая модель (необязательная) - формализует идею проекта;

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

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

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

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

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

Вид - это одна из проекций модели. В Рациональном Унифицированном Процессе существует пять тесно связанных друг с другом видов системной архитектуры:

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

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

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

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

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

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

 

 

Другие артефакты

Артефакты в Рациональном Унифицированном Процессе подразделяются на две группы: административные и технические. Технические артефакты, в свою очередь, делятся на четыре большие подгруппы:

- группа требований - описывает, что система должна делать;

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

- группа реализации - описывает сборку разработанных программных компонентов;

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

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

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

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

 

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

1.                 Что такое жизненный цикл программного обеспечения?

2.                 Как осуществляется выбор ЖЦ?

3.                 В чем состоит цель Рационального Унифицированного Процесса?

4.                 Как осуществляется определение и контроль конфигурации?

5.                 Перечислите основные Фазы, итерации и циклы разработки программного продукта.

6.                 Что представляют собой рабочие процессы?

7.                 Что такое артефакты?

 

 


Список литературы

 

1.                 Ашарина И.В. Основы программирования на языках С и С++.-М.: Горячая линия-Телеком, 2009.-207 с.

2.                 Боровский А.Н. С++ и Borland C++Builder. Самоучитель.-СПб.:Питер, 2009.-256 с.

3.                 Вендеров А.М. Проектирование программного обеспечения экономических информационных систем: Учебник.-2-е изд., перераб. и доп. – М.:Финансы и статистика, 2011.-544с.:ил.

4.                 Грэхем И. Объектно-ориентированные методы. Принципы и практика. 3-е издание.:Пер. с англ.-М.:Издательский дом "Вильямс", 2014.-880с.:ил.-Парал.тит.англ.

5.                 Иванова Г.С., Ничушкина Т.Н., Пугачев Е.К. Объектно-ориентированное программирование: Учебник для вузов.-2-е изд., перераб и доп/ Под ред. Г.С.Ивановой.-М.:Изд-во МГТУ им. Н.Э.Баумана, 2013.-368 с.

6.                 Крячков А.В. Программирование на С и С++. Практикум [Текст]: учебное пособие для вузов/ А.В. Крячков, И.В. Сухинина и др. – СПб.: Питер, 2010. – 358 с.

7.                 Пол А. Объектно-ориентированное программирование на С++, 2-е изд./Пер. с англ. - СПб.; "Невский диалект" - "Издательство БИНОМ", 2009 г. - 462 с.

8.                 Страуструп Б. Язык программирования С++ [Текст]: учебник/ Б. Страуструп. – СПб: Питер, 2012. – 991 с.

9.                 Фридман А.Л. Основы объектно-ориентированного программирования на языке С++. Изд.2-е перераб. и доп. - М.: Горячая линия - Телеком, 2011. - 232с.


Учебно-методическое пособие

(курс лекций)

 

 

 

 

 

Михайлюк Екатерина Андреевна

 

 

 

 

ПРОГРАММНАЯ ИНЖЕНЕРИЯ

 

 

 

 

 

 

Редактор: Иванова Н.И.

Компьютерный набор: Михайлюк Е.А.

Корректор: Иванова Н.И.

 

 

 

Подписано в печать___________ Бумага для множительной техники

Формат _______ Усл. печ. л. ________ Тираж _____ экз.  Заказ _____

 

 

 

 

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

в отделе оперативной печати СТИ НИТУ «МИСиС»

г. Старый Оскол, м-н Макаренко, 40

 

 

Просмотрено: 0%
Просмотрено: 0%
Скачать материал
Скачать материал "Курс лекций "Программная инженерия"."

Методические разработки к Вашему уроку:

Получите новую специальность за 2 месяца

Инженер по охране окружающей среды

Получите профессию

Няня

за 6 месяцев

Пройти курс

Рабочие листы
к вашим урокам

Скачать

Краткое описание документа:

Си является языком функций, типов данных, операторов присваивания и управления последовательностью вычислений. Большинство функций возвращают некоторые значения. Значение, возвращаемое функцией, может использоваться в операторе присваивания, который изменяет значение другой переменной. Си - язык высокого уровня, способствующий хорошему стилю программирования. Си имеет небольшой набор типов данных: целые числа, числа с плавающей запятой, битовые поля и перечислимый тип. Адресная арифметика языка Си является чувствительной к типу данных того объекта, с которым связан используемый указатель. Разрешены также указатели к функциям. Можно расширить список типов данных путем создания структур с иерархической зависимостью входящих в него типов данных. Каждый тип данных может принадлежать либо к основному типу, либо к ранее описанному структурному типу. Объединения напоминают структуры, но определяют различные виды иерархических зависимостей, в которых данные разных типов располагаются в памяти. Функции Си являются рекурсивными по умолчанию.

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

 

 

Скачать материал

Найдите материал к любому уроку, указав свой предмет (категорию), класс, учебник и тему:

6 661 890 материалов в базе

Скачать материал

Другие материалы

Вам будут интересны эти курсы:

Оставьте свой комментарий

Авторизуйтесь, чтобы задавать вопросы.

  • Скачать материал
    • 04.01.2015 1131
    • DOCX 1.8 мбайт
    • Оцените материал:
  • Настоящий материал опубликован пользователем Михайлюк Екатерина Андреевна. Инфоурок является информационным посредником и предоставляет пользователям возможность размещать на сайте методические материалы. Всю ответственность за опубликованные материалы, содержащиеся в них сведения, а также за соблюдение авторских прав несут пользователи, загрузившие материал на сайт

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

    Удалить материал
  • Автор материала

    Михайлюк Екатерина Андреевна
    Михайлюк Екатерина Андреевна
    • На сайте: 8 лет и 9 месяцев
    • Подписчики: 0
    • Всего просмотров: 3077
    • Всего материалов: 2

Ваша скидка на курсы

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

Курс профессиональной переподготовки

Бухгалтер

Бухгалтер

500/1000 ч.

Подать заявку О курсе
  • Сейчас обучается 22 человека из 16 регионов

Курс повышения квалификации

Специфика преподавания информатики в начальных классах с учетом ФГОС НОО

72 ч. — 180 ч.

от 2200 руб. от 1100 руб.
Подать заявку О курсе
  • Сейчас обучается 39 человек из 20 регионов
  • Этот курс уже прошли 284 человека

Курс повышения квалификации

Компьютерная грамотность для пенсионеров

36 ч. — 180 ч.

от 1580 руб. от 940 руб.
Подать заявку О курсе
  • Этот курс уже прошли 22 человека

Курс профессиональной переподготовки

Разработка и сопровождение требований и технических заданий на разработку и модернизацию систем и подсистем малого и среднего масштаба и сложности

Системный аналитик

600 ч.

9840 руб. 5600 руб.
Подать заявку О курсе
  • Сейчас обучается 64 человека из 34 регионов
  • Этот курс уже прошли 83 человека

Мини-курс

Основы гештальт-терапии: история и теория

5 ч.

780 руб. 390 руб.
Подать заявку О курсе
  • Сейчас обучается 42 человека из 21 региона
  • Этот курс уже прошли 16 человек

Мини-курс

Основы изучения творческих дисциплин: введение в пропедевтику дизайна и изобразительного искусства

8 ч.

1180 руб. 590 руб.
Подать заявку О курсе
  • Сейчас обучается 29 человек из 17 регионов
  • Этот курс уже прошли 12 человек

Мини-курс

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

10 ч.

1180 руб. 590 руб.
Подать заявку О курсе