Структурное и объектно-ориентированное программирование
ТЕХНОЛОГИИ ПРОГРАММИРОВАНИЯ
Классы и потоки ввода-вывода
Ввод-вывод в С++, также как и в С, рассматривается как поток данных, управляемый с помощью функций ввода-вывода. В С++ для поддержки ввода-вывода данных используется целая иерархия классов.
Основополагающим базовым классом является класс ios. Производными от него являются классы istream – поддерживает базовые операции ввода,
ostream – поддерживает базовые операции вывода. Двунаправленный поток поддерживается классом iostream, производным от istream и ostream, что можно представить схемой:
ios
____________|_________
| |
istream ostream
|_____________________|
|
iostream
Объявления этих классов ввода-вывода помещено в заголовочный файл, который нужно подключить к программе строкой:
#include<iostream.h>
Начав с определения потока как абстракции для моделирования прохождения потока данных от источника к приемнику, класс iostream обеспечивает иерархию классов для управления буферированным и непосредственным вводом-выводом для устройств и файлов. Пользователь может создавать собственные классы ввода-вывода на базе стандартных классов.
Стандартный ввод-вывод
С++ поддерживает четыре предопределенных потоковых объекта:
1. cin – стандартный ввод, как правило с клавиатуры, аналог stdin в С (объект класса istream).
2. cout – стандартный вывод, как правило на экран, аналог stdout в C (объект класса ostream).
3. cerr– стандартный небуферизованный вывод ошибок, как правило на экран, аналог stderr в С (объект класса ostream).
4. clog– буферизованный вывод ошибок, нет аналога в С (объект класса ostream).
Можно переназначать стандартные потоки на другие устройства и файлы.
Для каждого потока "перегружены" (переопределены) два оператора:
>>– оператор извлечения (чтения) из потока (для ввода с клавиатуры: "считать из");
<<– оператор записи (вставки) в поток (для вывода на экран: "вывести на").
Замещающие функции способны воспринимать аргументы любых основных типов данных (char, char*(string), int, long, float, double) и могут быть расширены для того, чтобы воспринимать аргументы типа class.
Ввод для встроенных типов
Файл заголовков iostream.h содержит объявление объекта вида:
istream cin;
Класс iostream определяет оператор ввода >> для набора стандартных типов:
class istream
{ // …
public: // общедоступные функции
istream & operator >> (char&); // символ
istream & operator >> (char*); // символьная строка
istream & operator >> (short&); // короткий целый
istream & operator >> (int&); // целый
istream & operator >> (long&); // длинный целый
istream & operator >> (float&); // действительный
istream & operator >> (double&); // двойной действительный
}
Операция >> (извлечения, чтения) заменяет функцию ввода scanf() и лучше защищает от ошибок. Левый операнд – это объект типа класса istream. Правый операнд может быть любого типа, для которого определен ввод потока. По умолчанию оператор >> опускает пробельные символы, а затем считывает символы, соответствующие типу вводимого данного. Функции преобразования и форматирования данных зависят от их типов.
Для ввода-вывода можно строить цепочки операций слева направо. Например, так можно ввести несколько данных в С++:
int i; float f;
cin >> I >> f;
Для всех числовых типов в случае, когда первый непробельный символ не является цифрой, или знаком, или точкой (для вещественных чисел) поток входит в состояние ошибки и вплоть до сброса состояния ошибки любой дальнейший ввод запрещен.
Для целых типов short, int, long действие операции >> по умолчанию заключается в пропуске пробельных символов и преобразовании целого значения путем чтения символов ввода до символа, который не является допустимым для данного типа.
Для типов с плавающей точкой float иdouble действие операции >> состоит в пропуске пробельных символов и преобразовании значения с плавающей точкой до символа, который не может быть частью числа с плавающей точкой.
Вывод для встроенных типов
Файл заголовков iostream.h содержит объявление объекта вида:
ostream cout;
Класс ostream определяет оператор вывода << для встроенных типов:
class ostream
{ // …
public: // общедоступные функции
ostream & operator << (char); // символ
ostream & operator << (char*); // символьная строка
ostream & operator << (int i); // целый
ostream & operator << (long);// длинный целый
ostream & operator << (double); // двойной действительный
ostream & put(char);// символ
}
Операция <<, называемая вставкой, заменяет функцию вывода printf(). Левый операнд – это объект cout типа класса ostream. Правый операнд может иметь любой тип, для которого определен вывод потоком для встроенных типов.
Функция-оператор << возвращает обращение по адресу, для которого она была вызвана, так что можно применить другой оператор ostream, т.е. можно строить цепочки операций вывода в порядке слева направо.
Например:
int i, k=234; double d ; char ch='A';
cout<<”Значение k=” << k <<’\n’; // в конце символ перевода строки
cin>> i >> d; // ввод данных i, d
cout<<”i=”<<i<<” d=”<<d<<’\n’; // вывод значений i, d
cout<<”ch=”<<ch; // вывод символа A
Пример 1.
Потоки ввода-вывода.
#include<iostream.h> // подключение библиотек ввода-вывода
void main()
{ int i;
char str[ ]=”Пример вывода в С++\n”;
cout<<”Вывод строки”<<str;
cout<<”Введите целое и длинное целое число: “;
long l; // переменная описывается перед первым использованием
cin>>i>>l; //ввод данных
cout<<”i=”<<i<<” l=”<<l<<’\n;
cout<<”Введите строку: ”;
cin>>str;
cout<<”Введена строка: ”<<str;
}
Замечание. Можно также использовать любые библиотечные функции ввода-вывода языка С (printf(), scanf() и др.). Кроме того, в другом контексте преопределенные операторы << и >> задают операции сдвига.
Пример 2.
Встроенные типы вставок (вывода данных). Целые типы преобразуются по правилам (по умолчанию) для printf (если эти правила не изменены путем установки флагов ios).
#include<iostream.h> // подключение библиотек ввода-вывода
#include<stdio.h> // для printf
#include<conio.h> // для консольных функций
void main()
{ int i=5; int *pi=&i; long l=12345678; double d=345.6789; char ch='A';
clrscr(); // чистка экрана результатов
cout<<”Вывод с помощью функции printf():\n”);
printf(“i=%d адрес i pi=%#x l=%ld d=%.4f ch=%c\n”, i,pi,l,d,ch);
cout<<”Вывод с помощью потока cout:\n”;
cout << ”i=”<< i << ” адрес i=” << &I << ” l =” << l << ” d=” <<d;
cout<<” ch=”<<ch<<'\n';
getch(); // задержка экрана результатов
}
Результаты программы:
Вывод с помощью функции printf():
i=5 адрес i pi=0xfff4 l=12345678 d=345.6789 ch=A
Вывод с помощью потока cout:
i=5 адрес i=0x8f61fff4 l=12345678 d=345.6789 ch=A
Управление вводом-выводом
Символьные извлечения.
Для типа char действие операции >> состоит в пропуске пробельных символов и чтения следующего (непробельного) символа. Если требуется прочесть следующий символ (любой), то можно использовать одну из функций-элементов get() класса istream:
сhar ch;
cin.get(ch); // ch устанавливается на следующий символ потока,
// даже если это пробельный символ.
Следующий вариант get позволяет управлять числом извлекаемых символов, их размещением и оконечным символом:
get(char* buf, int max, int term=’\n’).
Эта функция считывает символы из входного потока в символьный массив buf до тех пор, пока не будет считано max -1символов, либо пока не встретится символ, заданный term, в зависимости от того, что будет раньше. Завершающий пустой символ (нуль-байт – ‘\0’) добавляется автоматически. По умолчанию оконечным символом (терминатором), который не требуется задавать, является ‘\n’. Сам терминатор в массив buf не считывается и из входного потока istream не удаляется. Массив buf должен иметь размер не менее max символов.
Пример 3.
Ввод строки цифр и преобразование их в целое число типа int с помощью функции atoi(s) и вывод числа в 10-ом, 16-ом, 8-ом форматах с использованием идентификаторов dec, hex, oct, вставленных в поток вывода для управления форматом выходного потока, endlаналог'\n'. (см. Манипуляторы).
#include<iostream.h> // подключение библиотек ввода-вывода
#include<conio.h> // для консольных функций
#include<stdlib.h> // для функции atoi()
const int size=35; // размер буфера строки
void main()
{ clrscr();
int value; // переменная для числа
char s[size]; // буфер строки
cout<<"Value= ";
cin.get(s, size,'\n'); // ввод строки числа
value=atoi(s); // преобразование строки в число
cout<<"Decimal="<<dec<<value<<" Hexadecimal=0x"<<hex<<value
<<" Octal=0"<<oct<<value<<endl;
getch();
}
Результаты программы:
Value=123
Decimal=123 Hexadecimal=0x7b Octal=0173
Замечание. Для ввода строки вместо функции cin.get(s, size,'\n');можно использовать cin>>s. Этот оператор сработает, но если пользователь введет более 34 символов, оператор ввода продолжит запись за пределами буфера s, при этом, возможно, данные и код, располагающиеся за буфером, будут уничтожены. Такая операция может привести к краху системы!
Пример 4.
Использование функции get для безопасного чтения строк.
#include<iostream.h>
void main()
{ char s[35]; // буфер строки
char c; // переменная символа
cout<<"Введите строку не более 34 символов:\n";
cin.get(s, 35); // пропущен 3-й аргумент по умолчанию '\n'
cout<<"Вы ввели строку: "<<s<<endl;
if(cin.get(c) && c!='\n') // если следующий символ не '\n',то вывод
cout<<"Достигнута максимальная длина строки\n";
}
При вводе строки длиной более 34 символов она будет усечена и ввод будет безопасным. Одна проблема все же остается. Символ '\n' или другой символ, завершающий ввод, остается в потоке и должен быть прочитан еще одним оператором cin.get(), как показано последним оператором if. Если cin.get()не читает символ '\n',ввод будет усечен. Этот факт необходимо учитывать при написании программ.
Проблему непрочитанного символа '\n' можно решить с помощью метода cin.getline()с тремя параметрами (совпадет с get()), при этом символ разделитель '\n' также помещается в принимающую строку символов (s).
Для ввода неформатированных и непреобразованных двоичных данных используется функция cin.read((char*)&x, sizeof(x)).
Форматирование ввода/вывода.
Форматирование ввода и вывода определяется флагами состояния формата, перечисленными в классе ios, по умолчанию. Их можно изменить с помощью специальных функций.
Ширина вывода.
По умолчанию вставки выводят минимальное число символов, которыми может быть представлен операнд правой части. Для изменения используются функции:
int ios::width(int w); // устанавливает поле шириной w символов и возвращает предыдущую ширину,
int ios::width(); // возвращает предыдущую ширину, не внося изменений.
По умолчанию width=0, то есть вывод выполняется без заполнителей. При w≠0, если длина числа меньше w используются заполнители, иначе выводится фактическое число без усечения.
Например:
int i=123;
int oldw=cout.width(6);
cout<< i; // вывод будет 123 (перед числом 3 пробела), затем width=0
cout.width(oldw); // восстановление предыдущей ширины width=6
После каждой форматной вставки ширина обнуляется, например:
int i, j;
cout.width(4);
cout<< i <<" "<< j <<;
Здесь i будет выведено четырьмя символами, однако пробел и j будут иметь минимально необходимое число символов.
Заполнители и дополнение вправо и влево.
Символ-заполнитель и направление дополнения зависят от установок внутренних флагов, отвечающих за эти параметры.
По умолчанию символом-заполнителем является пробел. Изменить данное умолчание позволяет функция fill:
int i=123;
cout.fill ("*");
cout.width(6);
cout<< i; // на экран будет выведено ***123
По умолчанию устанавливается выравнивание по правому краю (дополнение символами-заполнителями). Это можно изменить функциями setf и unsetf:
int i=56;
cout.width(6);
cout.fill ("#");
cout.setf (ios::left, ios::adjustfield);
cout<< i; // на экране: 56####
Второй аргумент сообщает setf, какие биты флага должны быть установлены. Первый аргумент сообщает, в какие именно значения устанавливаются эти биты.
Можно также использовать манипуляторы setfill, setiosflags, resetiosflags.
Манипуляторы.
Это специальные операции (похожие на функции) для более простого изменения ширины и других параметров форматирования. Для их использования программа должна иметь строку:
#include<iomanip.h>
Манипуляторы принимают в качестве аргумента ссылку на поток и возвращают ссылку на тот же поток. Поэтому манипуляторы могут объединяться в цепочку вставок (или извлечений из потока) для того, чтобы изменять состояние потока в виде побочного эффекта, без фактического выполнения каких-либо вставок или извлечений. В таблице ниже в столбце синтаксис задано направление потока: входной – ins >>, выходной – outs <<.
Таблица
Манипулятор Синтаксис Действие
Установка флага форматирования с преобразованиями:
dec outs << dec 10-ым ins >> dec
hex outs << hex 16-ым ins >> hex
oct outs << oct 8-ым ins >> oct
ws ins >> ws Извлечение пробелов
endl outs << endl Вставка символа новой строки
и очистка потока
ends outs << ends Вставка конечного нулевого
символа в строку
flush outs << flush Очистка ostream
setbase(int) outs << setbase(n) Установка системы счисления
(0, 8, 10, 16). 0 это по умолча-
нию 10-ая с.с. при выводе и
правила С для литералов це-
лых чисел при вводе
resetiosflags(long) ins>>resetiosflags(1) Очистка битов в ins или outs
outs<<resetiosflags(1) аргументом 1
setiosflags(long) ins >> setiosflags(1) Установка битов в ins или outs
outs << setiosflags(1) аргументом 1
setfill(int) ins >> setfill(n) Установка символа
outs >> setfill(n) заполнителя в n
setprecision(int) ins >> setprecision(n) Установка точности представле-
outs>>setprecision(n) ния чисел с плавающей точкой,
равной n разрядам
setw(int) ins >> setw(n) Установка ширины поля
outs << setw(n) в значение n
Пример 5.
Применение функций и манипуляторов форматирования вывода в программе.
#include<iostream.h>
#include<conio.h>
#include<iomanip.h>
void main()
{ clrscr();
int i=36, j=45;
cout<<"Вывод с установленной шириной поля 5 числа i:\n";
int oldw=cout.width(5); // установка ширины поля вывода
cout<< i <<'\n';
cout<<"После каждой вставки формата ширина поля=0:\n";
cout<< " oldw=" << oldw << '\n';
cout<< i << '\n';
cout<< "Установка поля 5 влияет только на 1-ю переменную:\n";
cout.width(5);
cout<< i << " " << j << '\n';
cout<< "Манипулятор setw(5) упрощает вывод переменных:\n";
cout<< setw(5) << i << setw(5) << j << '\n';
cout<< "Манипуляторы dec, hex, oct изменяют сист. счисл. \n";
cout<< "(оставляя эти изменения в силе) — вывод i:\n";
cout<< dec << i << " " << hex << i << " " << oct << i <<endl;
cout<< "Манипулятор endl аналог '\n' очищает поток"<<endl;
cout<< "i= " << i <<endl;
cout<< "Заполнитель пробел можно заменить на *"<<endl;
cout.fill ( '*' ); // задание символа заполнения функцией fill
cout<< setw(6) << i << endl; // установка ширины поля 6 числа i
cout<< "Выравнивание можно изменить функцией setf:"<<endl;
cout.width(6);
cout.setf(ios::left, ios::adjustfield);
cout<< "i= " << i <<endl;
getch();
}
Вывод чисел с плавающей точкой.
Большие и малые числа выводятся в экспоненциальном формате (е, Е), а не фиксированном. Например, число 1234567.8 печатается 1.2345678Е+07.
Числа с нулевой дробной частью печатаются как целые. Например, число 95.0 печатается 95.
Для вывода десятичных чисел в фиксированном формате можно использовать следующие операторы:
cout.setf(ios::fixed, ios::floated);
cout.setf(ios::showpoint);
Первый вызов функции setf обеспечивает печать чисел в фиксированном, а не экспоненциальном представлении. Второй вызов задает печать десятичной точки, даже если дробная часть числа равна 0.
Управление числом десятичных позиций при выводе выполняется с помощью манипулятора установки точности:
cout<< setprecision(2) << x;
например, вместо числа 16.38567 печатается 16.39.
Пример 6.
Если отсутствует переназначение, то ввод с клавиатуры отображается на экран. Следующая программа копирует cin в cout.
#include<iostream.h>
void main()
{ char ch;
while (cin >> ch)
cout << ch;
}
Обратите внимание, что cin >> ch может рассматриваться как булевское выражение. Этот эффект стал возможен благодаря определениям в классе ios. А именно, такие выражения как (cout) или (cin >> ch) рассматриваются как указатель на переменную, значение которой определяется кодом ошибки потока. Нулевой указатель (трактуемый как "ложь") индицирует ошибку в потоке, а ненулевой указатель ("истина") означает ее отсутствие. Можно использовать операцию отрицания (!), в этом случае (!cout) будет "истина" при возникновении ошибки в потоке cout и "ложь", если они отсутствуют:
if (!cout) errmsg("Output error!");
cin — это входной поток, подключенный к стандартному выводу. Он может правильно обрабатывать все стандартные типы данных. Как известно, в С вывод подсказки (приглашения) без символа новой строки ('\n') в стандартный выходной поток stdout требует обращения к fflush(stdout), чтобы эта подсказка могла появиться. В С++ обращение к cin автоматически очищает cout.
Рассмотрим примеры простых программ на С++ с отличиями от С-программ.
Пример 7.
Применение констант и встроенных функций (inline).
#include<iostream.h>
const float pi=3.14159;
inline float area (const float r) { return pi*(r)*(r); }
void main()
{ float radius;
cout << "Введите радиус круга: ";
cin >> radius;
cout << "Площадь круга= " << area (radius) << '\n';
}
Идентификатор константы ведет себя как обычная переменная (т.е. ее областью действия является блок, в котором она определена) за исключением того, что она не может находиться в левой части оператора присваивания. Директива #define является морально устаревшей для С++.
Ключевое слово inline указывают компилятору, что, если возможно, следует вставлять код в том месте, где он встретил обращение. Это эквивалентно макрорасширению и используется там, где необходимо ликвидировать накладные расходы, связанные с вызовом функции. Рекомендуется применять встроенную функцию, составленную из одной-двух строк.
Ссылки и указатели как параметры функции.
По умолчанию С и С++ передают аргументы функции, используя вызов по значению, чем создается копия аргумента, которая может быть изменена в функции, но исходное значение аргумента не меняется. Для изменения значения аргументов параметры функции объявляются как указатели (*параметр).
Пример 8.
Переставить значения двух аргументов в функции с указателями.
void swap1 (int *a, int *b)
{ int tmp;
tmp=*a; *a=*b; *b=tmp;
}
Для вызова функции необходимо задать адреса аргументов:
swap1(&i, &j); // i, j — аргументы функции
В С++ можно использовать в функции ссылки (&) на изменяемые параметры, а в теле функции и при вызове применяются имена параметров и аргументов:
void swap2 (int &a, int &b)
{ int tmp=a; a=b; b=tmp; }
Вызов функции:
swap2 (i, j);
Список элементов класса
Список элементов представляет собой последовательность объявлений данных (любого типа, включая нумераторы, битовые поля и другие классы) и объявлений и определений функций, каждое из которых может иметь необязательные модификаторы доступа. Определенные таким образом объекты называют элементами (членами) класса (class members). Спецификаторы класса памяти auto, register, extern недопустимы. Элементы могут быть объявлены со спецификатором static.
Функция-элемент класса называется методом класса, а функция с модификатором friend называется дружественной функцией. Данные класса – это то, что класс "знает " (пассивная часть класса), а методы – это то, что класс "умеет делать" (активная часть).
Определение методов класса
Существует два способа включения метода в класс.
Первый способ – метод может быть определен внутри класса как встраиваемая функция с одним-двумя операторами. Компилятор осуществляет макрорасширение таких функций, снижая накладные расходы, связанные с обращением к функции и выходом из нее, например:
class Point
{ int X, Y;
int Getx() { return X; } // встроенная функция
}
Второй способ – объявление метода внутри класса в виде прототипа функции и определение метода в произвольном месте программы с помощью операции определения области действия функции ::(два двоеточия), например:
class Point
{ int X, Y;
int Getx(); // объявление метода
}
int Point :: Getx() // определение метода
{ return X; } // вне класса
Имя Point :: Getx называется полным или квалифицированным именем метода класса. В С++ разные функции могут иметь функции с одинаковыми именами. Операция :: и имя класса позволяют компилятору определить принадлежность функции конкретному классу.
Определение метода внутри класса (способ 1) не требует модификатора Point::, так как и так очевидно, что Getx принадлежит классу Point. Кроме того, модификатор Point:: перед Getx служит и другой цели. Он определяет, что переменная Х в операторе return X является собственным элементом класса Point, к которому можно обращаться непосредственно по имени.
Вывод. Тело метода Point::Getx() находится внутри области действия класса Point вне зависимости от физического расположения. Метод Getx() имеет доступ ко всем внутренним переменным класса Point.
Конструктор по умолчанию
Конструктор, не имеющий параметров, называется конструктором по умолчанию. Если отсутствует определенный пользователем конструктор по умолчанию, компилятор С++ генерирует собственный конструктор по умолчанию для описанного класса – это конструктор, например, класса Х, который не принимает никаких аргументов X :: X().
Конструктор, как и любая функция, может иметь любое количество параметров, в том числе и параметры по умолчанию. Например, конструктор
X :: X (int, int=0)
может принимать один или два аргумента. Если будет только один аргумент, то недостающий второй аргумент будет принят как 0. Аналогично, конструктор
X :: X (int=5, int=6)
может принимать два аргумента, один аргумент, либо не принимать аргументов вообще, причем в каждом случае принимаются значения, соответствующие умолчаниям.
Однако конструктор по умолчанию X :: X() не принимает аргументов вообще, и его не следует путать с конструктором, например, X :: X (int=0), который может либо принимать один аргумент, либо не принимать аргументов. При вызове конструкторов следует избегать неоднозначностей. В следующем примере возможна неоднозначная трактовка компилятором конструктора по умолчанию и конструктора, принимающего целый параметр.
Пример 15.
Неоднозначность конструкторов.
class X
{ public:
X();
X( int = 0);
};
void main()
{ X one (10); // так МОЖНО: используется конструктор X::X(int)
X two; // так НЕЛЬЗЯ: X::X() или X::X(int=0) ?
}
Конструктор копирования
Это особый тип конструктора. Конструктор копирования для класса Х – это конструктор, который имеет один аргумент ссылочного типа X& и, возможно, параметры по умолчанию. Ссылочный тип тесно связан с типом указатель (X*) и позволяет передавать в функцию не сам объект, а только ссылку на него.
По умолчанию, объекты класса можно копировать. В частности, объект некоторого класса можно проинициализировать при помощи копирования объекта того же класса. Это можно сделать даже там, где объявлен конструктор.
Например, для класса X корректное описание конструктора копирования может быть таким:
class X
{ public:
X() {...} // конструктор по умолчанию
X(const X&) {...} // конструктор копирования
X(const X&, int i=4) {...} // конструктор копирования с параметром // по умолчанию
};
Конструктор копирования вызывается тогда, когда выполняется копирование объекта, обычно при объявлении объекта с его инициализацией, например:
X one; // вызов конструктора по умолчанию
X two = one; // вызов конструктора копирования
X two (one); // вызов конструктора копирования с параметром
По умолчанию, копия объекта класса содержит копию каждого члена, т.е. объект two будет полной копией объекта one. Аналогично, объекты класса могут по умолчанию почленно копироваться при помощи операции присваивания:
X three;
three = one;
Подобное почленное копирование приемлемо, если оно выполняется для статических и автоматических переменных класса. Выделение динамической памяти для объектов с использованием указателей и их копирование будет рассмотрено ниже.
Множественное наследование
В рассмотренном выше примере классы X и Y по сути равноправны, но при использовании простого наследования класс Y отличается от класса X числом параметров конструктора. Избавится от неравноправия классов можно, используя множественное наследование. Суть его в том, что производный класс может быть наследником нескольких базовых классов, например, для классов X, Y, Z по схеме наследования (X, Y) <- Z, которая в программе может принимать вид:
class Z : public X, public Y
{ тело класса Z };
Пример 30.
Изменим программу примера 29 в соответствии со схемой множественного наследования (X, Y) <- Z.
#include<iostream.h>
#include<conio.h>
class X // базовый класс X для класса Z
{ protected: int x; // защищенный элемент х, доступный в Z
public: // прототипы открытых методов класса X:
X(int i); // конструктор с параметром класса X
~X(); // деструктор класса X
int getx () { return x; } // встроенная функция возврата x
void putx (int i) { x=i; } // встроенная функция задания x
void show(); // функция вывода на экран для класса Х
};
class Y // базовый класс Y для класса Z
{ protected: int y; // защищенная переменная, доступная в Z
public: // прототипы открытых методов класса Y:
Y(int i); // конструктор с параметром класса Y
~Y(); // деструктор класса Y
int gety () { return y; } // встроенная функция возврата y
void puty (int i) { y=i; } // встроенная функция задания y
void show(); // функция вывода на экран для класса Y
};
// Класс Z производный от Х и Y имеет доступ к переменным Х::x и Y::у:
class Z : public Х, public Y
{ protected: int z; // защищенная переменная z
public: // прототипы открытых методов класса Z:
Z(int i, int j); // конструктор с параметрами класса Z
~Z(); // деструктор класса Z
void makez (); // функция вычисления z
void show(); // функция вывода на экран для класса Z
};
// Описание методов:
X::X(int i) // конструктор с параметром класса X
{ x = i; cout<<"Конструктор X\n"; } // инициализация элемента х
X::~X() // деструктор класса X
{ cout<<"Деструктор X\n"; }
void X::show() // функция вывода на экран класса Х
{ cout<<" x = "<< x <<endl; }
Y::Y(int i): // конструктор с параметром класса Y
{ y = i; cout<<"Конструктор Y\n"; } // инициализация элемента у
Y::~Y() // деструктор класса Y
{ cout<<"Деструктор Y\n"; }
void Y::show() // функция вывода на экран класса Y
{ cout<<" y = "<< y <<endl; }
Z::Z(int i, int j):X(i), Y(j) // конструктор Z с параметрами i, j для классов X, Y
{ cout<<"Конструктор Z\n"; }
Z::~Z() // деструктор для класса Z
{ cout<<"Деструктор Z\n"; }
void Z::show() // функция вывода на экран класса Z
{ cout<<" z = "<< z <<endl; }
void Z::makez() // функция вычисления переменной z
{ z=x * y; } // с использованием переменных из X,Y
void main() // главная функция
{ clrscr(); // чистка экрана
Z zobj(3, 5);; // создан и инициирован объект класса Z
zobj.makez(); // вычисление z=x*y
zobj.show(); // вызов функции вывода для класса Z
zobj. X::show(); // вызов функции вывода для класса X !
zobj. Y::show(); // вызов функции вывода для класса Y !
zobj.putx(7); // изменение переменной х в классе Х
zobj.puty(9); // изменение переменной у в классе Y
zobj.makez(); // вычисление z=x*y
zobj.show(); // вызов функции вывода для класса Z
zobj. X::show(); // вызов функции вывода для класса X !
zobj. Y::show(); // вызов функции вывода для класса Y !
getch(); // задержка экрана результатов
}
Результаты программы:
Конструктор X
Конструктор Y
Конструктор Z
z=15
x=3
y=5
z=63
x=7
y=9
Деструктор Z
Деструктор Y
Деструктор X
ТЕХНОЛОГИИ ПРОГРАММИРОВАНИЯ
Структурное и объектно-ориентированное программирование
Разработка программного обеспечения (ПО) ЭВМ в настоящее время осуществляется с использованием двух основных технологий — структурного (процедурного) программирования и объектно-ориентированного программирования (ООП).
Структурное программирование.
Данная технология предполагает выполнение последовательности этапов разработки программ для решения задач с использованием ЭВМ.
1. Постановка задачи – формулирование задачи и целей ее решения на естественном языке и установление критериев решения задачи. Результат этапа – техническое задание на разработку ПО.
2. Формализация задачи с использованием математического аппарата и получение ее абстрактной математической модели в виде формул и уравнений.
3. Выбор численного метода из возможных вариантов с учетом требований по времени и точности решения и занимаемого объема памяти ЭВМ.
4. Алгоритмизация – построение общего плана решения, т. е. алгоритма задачи в виде логической последовательности этапов (шагов, действий, операций), приводящих от исходных данных к искомому результату за конечное время на языке понятном человеку. Могут быть использованы различные способы представления алгоритма – словесный, графический (схемы алгоритмов), алгоритмический язык высокого уровня (ЯВУ). ЯВУ – формализованный язык для описания данных и набор правил (инструкций, операторов) их обработки для реализации алгоритма задачи. Типичные ЯВУ структурного программирования – Си, Паскаль.
5. Программирование – перевод алгоритма задачи на язык ЭВМ (систему команд), т.е. кодирование алгоритма. Процесс разработки программы делится на следующие этапы: 1) запись алгоритма на ЯВУ в виде исходного файла в памяти (например, prog1.cpp); 2) компиляция и редактирование связей (объектный файл – prog1.obj); 3) загрузка программы в оперативную память (исполняемый файл – prog1.exe); 4) исполнение программы; 5) получение результатов программы.
6. Отладка программы – поиск и исправление ошибок в программе. Этот процесс разбивается на два этапа: 1) синтаксическая отладка – исправление формальных ошибок, связанных с нарушением норм языка программирования, с помощью ЭВМ; 2) семантическая отладка – исправление логических (смысловых) ошибок с применением специальных тестовых данных.
7. Исполнение (эксплуатация) программы с любыми допустимыми данными и получение результатов решения задачи.
8. Интерпретация результатов и поддержка программы в процессе эксплуатации – изменение программы в соответствии с требованиями пользователей, а также исправление ошибок, выявленных в процессе ее эксплуатации.
Существование программы можно разделить на три периода:
1) разработка (этапы 1– 4); 2) реализация (этапы 5, 6); 3) сопровождение (этапы 7, 8).
Функциональная декомпозиция.
При решении сложной задачи разработка и реализация ее алгоритма потребует написания длинной программы, которую трудно отлаживать из-за возможного большого числа ошибок. Внесение изменений вызовет необходимость дополнительного выполнения этапов разработки и реализации программы в целом.
Целесообразно такую задачу разбить на легко решаемые подзадачи, которые в совокупности дают решение исходной задачи. Такой метод решения задач называется функциональной декомпозицией. Для применения этого метода на ЭВМ используется принцип модульного программирования. Каждая подзадача реализуется в виде отдельной подпрограммы (функции, процедуры). Для решения всей задачи создается главная функция, которая вызывает другие функции, передавая им исходные аргументы и получая промежуточные результаты.
Большую программу целесообразно разделить на несколько программных модулей (автономно компилируемых файлов), например, файл подпрограмм и файл главной функции, а для их соединения в общую программу создается файл проекта. При этом файл подпрограмм можно рассматривать как библиотеку готовых подпрограмм, которые можно использовать в других задачах, что сократит время их решения.
Для структурного программирования характерно то, что данные и методы их обработки (функции, процедуры) отделены. Данные рассматриваются как пассивные элементы, обрабатываемые функциями.
Концепции объектно-ориентированного программирования (ООП).
Идеи ООП впервые были выдвинуты и реализованы в 60-е годы 20-го века авторами языков Simula-67 (Дал и Нигард) и Smaltalk.
Главная причина возникновения ООП связана с поиском путей для создания очень сложных программ, отражающих сложность мира и способов его описания. ООП представляет собой технологию программирования, в основе которой лежит подход, позволяющий людям формировать модели объектов реального мира. Чтобы справиться со сложностями жизни, человечеству потребовалось выработать способность обобщать, классифицировать и генерировать абстракции.
ООП базируется на следующих ключевых понятиях: 1) объект (object);
2) класс (class); 3) инкапсуляция (encapsulation); 4) наследование (inharitance);
5) полиморфизм (polimorphism); 6) абстракция типов (abstraction).
Раскроем эти понятия в общем плане.
Объект. Наш мир состоит из объектов, которые обладают некоторыми свойствами и которым присуще определенное поведение. Объект — это логическая единица, которая содержит совокупность данных (атрибутов), определяющих его отличительные свойства, и правила, описывающие его поведение. В программе объект содержит данные (DATA) и код (CODE), который манипулирует этими данными.
Класс. Объекты, обладающие некоторым общим набором атрибутов (отличительных черт) и схожим поведением объединяют в классы объектов. Например, из реального мира, в котором существует множество конкретных собак, можно выделить абстрактный класс СОБАКА, отличающийся от классов других животных. Такой подход позволяет разрабатывать и развивать идеи, касающиеся собак, абстрагируясь от особенностей отдельной особи. Таким образом, класс – это новый обобщенный тип объектов. Он определяет, каким образом ведут себя любые объекты этого типа, как они создаются (порождаются), как с ними взаимодействуют другие объекты, как они уничтожаются. После описания класса можно объявить его конкретные экземпляры-объекты.
Инкапсуляция – это объединение набора данных и структур данных с группой методов (функций) манипулирования этими данными в единое целое, называемое классом, в котором также определены механизмы защиты данных и функций, которые могут быть частными (private), общими (public), или защищенными (protected) и установлены права доступа к данным.
Наследовании – это механизм создания новых, производных классов, которые наследуют данные и функции от одного или нескольких ранее определенных базовых классов. При этом возможны переопределение или добавление новых данных м методов. В результате создается разветвленная иерархия классов (дерево классов), корнем которой является наиболее общее понятие.
Например, СОБАКА
____________________|____________________
| | |
сто