Передача массивов в качестве параметров
Массив всегда передается по адресу. При этом информация о количестве элементов массива теряется, и следует передавать его размерность через отдельный параметр:
#include <iostream>
using namespace std;
int sum(const int* mas, const int n);
int const n = 10;
void main()
{
int marks[n] = {3, 4, 5, 4, 4};
cout << "Сумма элементов массива: "
<< sum(marks, n);
}
int sum(const int* mas, const int n)
/*варианты: int sum(int mas[], int n) или */
/*int sum(int mas[n], int n) (n должна быть константой) */
{ int s = 0;
for (int i = 0 ; i<n; i++) s += mas[i];
return s;}
При передаче многомерных массивов все размерности, если они не известны на этапе компиляции, должны передаваться в качестве параметров.
Внутри функции массив интерпретируется как одномерный, а его индекс пересчитывается в программе. В приведенном ниже примере с помощью функции подсчитывается сумма элементов двух двумерных массивов. Размерность массива b известна на этапе компиляции, под массив a память выделяется динамически:
#include <cstdio> using namespace std;
int sum(const int *a, const int nstr, const int nstb);
void main()
{
int b[2][2] = {{2, 2}, {4, 3}};
/* имя массива передавать нельзя из-за несоответствия типов */
printf("b %d\n", sum(&b[0][0], 2, 2));
int i, j, nstr, nstb, *a;
printf("Введите количество строк и столбцов: \n");
scanf("%d%d", &nstr, &nstb);
a = (int *)malloc( nstr*nstb*sizeof(int) );
for (i = 0; i<nstr; i++)
for (j = 0; j<nstb; j++)
scanf("%d", &a[i*nstb+j]);
printf("a %d\n", sum(a, nstr, nstb));
}
int sum(const int *a, const int nstr, const int nstb)
{int i, j, s = 0;
for (i = 0; i<nstr; i++)
for (j = 0; j<nstb; j++)s += a[i*nstb + j];
return s;
}
Передача имен функций в качестве параметров
Функцию можно вызвать через указатель на нее. Для этого объявляется указатель соответствующего типа и ему с помощью операции взятия адреса присваивается адрес функции:
void f(int a ){ /* * */ } //определение функции
void (*pf)(int); //указатель на функцию
...
pf = &f; /* указателю присваивается адрес */
/*функции (можно написать pf = f;) */
pf(10); /* функция f вызывается через указатель pf */
/*(можно написать (*pf)(10) ) */
Для того чтобы сделать программу более читаемой, при описании указателей на функции используют переименование типов ( typedef ). Можно объявлять массивы указателей на функции (это может быть полезно, например, при реализации меню):
/* описание типа PF как указателя на функцию с одним параметром типа int */
typedef void (*Pf)(int);
/* описание и инициализация массива указателей */
PF menu[]={&new, &open, &save}
menu[1](10); //вызов функции open
Указатели на функции передаются в подпрограмму таким же образом, как и параметры других типов:
void fun(PF pf) /* функция fun получает в качестве*/
/* параметра указатель типа PF */
{* pf(10); *} //вызов функции, переданной через указатель
Тип указателя и тип функции, которая вызывается посредством него, должны совпадать в точности.
Параметры со значениями по умолчанию
Чтобы упростить вызов функции, в ее заголовке можно указать значения параметров по умолчанию. Эти параметры должны быть последними в списке и могут опускаться при вызове функции. В качестве значений параметров по умолчанию могут использоваться константы, глобальные переменные и выражения:
int f(int a, int b = 0);
void f1(int, int = 100, char* = 0);
void err(int errValue = errno); // errno - глобальная переменная
f(100); f(a, 1); // варианты вызова функции f
// варианты вызова функции f1
f1(a);
f1(a, 10);
f1(a, 10, "Vasia");
f1(a,,"Vasia") // неверно!
Лекция 6. Объектно-ориентированное программирование
Свойства ООП
Основными свойствами ООП являются инкапсуляция, наследование и полиморфизм. Ниже кратко поясняется их смысл, а полное представление о них можно получить после изучения этой и следующей лекций.
Объединение данных с функциями их обработки в сочетании со скрытием ненужной для использования этих данных информации называется инкапсуляцией ( encapsulation ). Инкапсуляция повышает степень абстракции программы: данные класса и реализация его функций находятся ниже уровня абстракции, и при написании программы информация о них не нужна. Кроме того, инкапсуляция позволяет изменить реализацию класса без модификации основной части программы, если интерфейс остался прежним (например, при необходимости сменить способ хранения данных с массива на стек).
Инкапсуляция позволяет использовать класс в другом окружении и быть уверенным, что класс не испортит не принадлежащие ему области памяти, а также создавать библиотеки классов для применения во многих программах.
Наследование - возможность создания иерархии классов, когда потомки наследуют все свойства своих предков, могут их изменять и добавлять новые. Свойства при наследовании повторно не описываются, что сокращает объем программы. Выделение общих черт различных классов в один класс-предок является мощным механизмом абстракции - ведь и любая наука начинается с абстрагирования и классификации, которые помогают справиться со сложностью рассматриваемой предметной области.
Иерархия классов представляется в виде древовидной структуры, в которой более общие классы располагаются ближе к корню, а более специализированные - на ветвях и листьях. В С++ каждый класс может иметь сколько угодно потомков и предков. Иногда предки называются надклассами или суперклассами, а потомки - подклассами или субклассами.
Третьим китом, на котором стоит ООП, является полиморфизм - возможность использовать в различных классах иерархии одно имя для обозначения сходных по смыслу действий и гибко выбирать требуемое действие во время выполнения программы.
Понятие полиморфизма используется в С++ весьма широко. Простым примером полиморфизма может служить перегрузка функций, когда из нескольких вариантов выбирается наиболее подходящая функция по соответствию ее прототипа передаваемым параметрам. Другой пример - использование шаблонов функций, когда один и тот же код видоизменяется в соответствии с типом, переданным в качестве параметра. Чаще всего понятие полиморфизма связывают с механизмом виртуальных методов.
Использование при программировании понятий, более близких к предметной области, благодаря представлению программы в терминах поведения объектов является большим преимуществом ООП. Однако проектирование такой программы представляет собой весьма сложную задачу, поскольку в процесс добавляется еще один важный этап - разработка иерархии классов.
Плохо спроектированная иерархия приводит к созданию сложных и запутанных программ. Важно до начала проектирования правильно определить, требуется ли вообще применять объектно-ориентированный подход. Если в иерархии классов нет необходимости, то, как правило, достаточно ограничиться модульной технологией.
Сейчас мы перейдем к рассмотрению фундамента, без которого невозможно написать ни одну объектно-ориентированную программу - синтаксических правил описания объектов, а потом вернемся к обсуждению принципов ООП и методов проектирования объектных программ, поскольку "только хорошее понимание идей, стоящих за свойствами языка, ведет к мастерству" (Б. Страуструп).
Описание класса
Класс - это описание определяемого типа. Любой тип данных представляет собой множество значений и набор действий, которые разрешается выполнять с этими значениями. Например, сами по себе числа не представляют интереса - нужно иметь возможность ими оперировать: складывать, вычитать, вычислять квадратный корень и т. д. В С++ множество значений нового типа определяется задаваемой в классе структурой данных, а действия с объектами нового типа реализуются в виде функций и перегруженных операций С++.
Данные класса называются полями (по аналогии с полями структуры), а функции класса - методами. Поля и методы называются элементами класса. Описание класса в первом приближении выглядит так:
class <имя>{
[ private: ]
<описание скрытых элементов>
public:
<описание доступных элементов>
}; // Описание заканчивается точкой с запятой
Спецификаторы доступа private и public управляют видимостью элементов класса. Элементы, описанные после служебного слова private, видимы только внутри класса. Этот вид доступа принят в классе по умолчанию. Интерфейс класса описывается после спецификатора public. Действие любого спецификатора распространяется до следующего спецификатора или до конца класса. Можно задавать несколько секций private и public, порядок их следования значения не имеет.
Поля класса:
· могут быть простыми переменными любого типа, указателями, массивами и ссылками (т.е. могут иметь практически любой тип, кроме типа этого же класса, но могут быть указателями или ссылками на этот класс);
· могут быть константами (описаны с модификатором const ), при этом они инициализируются только один раз (с помощью конструктора) и не могут изменяться;
· могут быть описаны с модификатором static, но не как auto, extern и register.
Инициализация полей при описании не допускается.
Классы могут быть глобальными (объявленными вне любого блока) и локальными (объявленными внутри блока, например, внутри функции или внутри другого класса). Обычно классы определяются глобально.
Локальные классы имеют некоторые особенности:
· локальный класс не может иметь статических элементов;
· внутри локального класса можно использовать из охватывающей его области типы, статические ( static ) и внешние ( extern ) переменные, внешние функции и элементы перечислений;
· запрещается использовать автоматические переменные из охватывающей класс области;
· методы локальных классов могут быть только встроенными ( inline );
· если один класс вложен в другой класс, они не имеют каких-либо особых прав доступа к элементам друг друга и могут обращаться к ним только по общим правилам.
В качестве примера создадим класс, моделирующий персонаж компьютерной игры. Для этого требуется задать его свойства (например, количество щупалец, силу или наличие гранатомета) и поведение. Естественно, пример будет схематичен, поскольку приводится лишь для демонстрации синтаксиса.
class monster
{
int health, ammo;
public:
monster(int he = 100, int am = 10)
{ health = he; ammo = am;}
void draw(int x, int y, int scale, int position);
int get_health(){return health;}
int get_ammo(){return ammo;}};
В этом классе два скрытых поля - health и ammo, получить значения которых извне можно с помощью методов get_health() и get_ammo(). Доступ к полям с помощью методов в данном случае кажется искусственным усложнением, но надо учитывать, что полями реальных классов могут быть сложные динамические структуры, и получение значений их элементов не так тривиально. Кроме того, очень важной является возможность вносить в эти структуры изменения, не затрагивая интерфейс класса.
Методы класса имеют неограниченный непосредственный доступ к его полям. Внутри метода можно объявлять объекты, указатели и ссылки как своего, так и других классов.
В приведенном классе содержится три определения методов и одно объявление (метод draw ). Если тело метода определено внутри класса, он является встроенным ( inline ). Как правило, встроенными делают короткие методы. Если внутри класса записано только объявление (заголовок) метода, сам метод должен быть определен в другом месте программы с помощью операции доступа к области видимости:
void monster::draw(int x, int y, int scale, int position)
{ /* тело метода */}
Встроенные методы можно определить и вне класса с помощью директивы inline (как и для обычных функций, она носит рекомендательный характер):
inline int monster::get_ammo()
{return ammo;}
Методы можно перегружать (это одно из проявлений полиморфизма ), а также объявлять либо константными, либо статическими (но не одновременно).
В каждом классе есть метод, имя которого совпадает с именем класса. Он называется конструктором и вызывается автоматически при создании объекта класса. Конструктор предназначен для инициализации объекта. Автоматический вызов конструктора позволяет избежать ошибок, связанных с использованием неинициализированных переменных. Подробнее конструкторы описываются далее в разделе "Конструкторы".
Типы данных struct и union являются специальными видами класса.
Описание объектов
Конкретные переменные типа данных "класс" называются экземплярами класса, или объектами. Время жизни и видимость объектов зависит от вида и места описания и подчиняется общим правилам С++:
monster Vasia; // Объект класса monster с параметрами по умолчанию
monster Super(200, 300);// Объект с явной инициализацией
monster stado[100]; // Массив объектов с параметрами по умолчанию
/* Динамический объект (второй параметр задается по умолчанию) */
monster *beavis = new monster (10);
monster &butthead = Vasia; // Ссылка на объект
При создании каждого объекта выделяется память, достаточная для хранения всех его полей, и автоматически вызывается конструктор, выполняющий их инициализацию. Методы класса не тиражируются. При выходе объекта из области действия он уничтожается, при этом автоматически вызывается деструктор (деструкторы описаны далее).
Доступ к открытым ( public ) элементам объекта аналогичен доступу к полям структуры. Для этого используются операция . (точка) при обращении к элементу через имя объекта и операция -> при обращении через указатель:
объект.поле
указатель -> поле
(*указатель).поле
объект.метод( параметры )
указатель -> метод( параметры )
(*указатель).метод( параметры )
Обращение к открытому полю и вызов метода для массива объектов:
имя_массива[ индекс ].поле
имя_массива[ индекс ].метод( параметры )
Например:
int n = Vasia.get_ammo();
stado[5].draw;
cout << beavis->get_health();
Получить или изменить значения private элементов можно только через обращение к соответствующим методам.
Можно создать константный объект,значения полей которого изменять запрещается. К нему должны применяться только константные методы:
class monster{
...
int get_health() const {return health;}
};
const monster Dead (0,0); // Константный объект
cout << Dead.get_health();
Константный метод:
· объявляется с ключевым словом const после списка параметров;
· не может изменять значения полей класса;
· может вызывать только константные методы;
· может вызываться для любых (не только константных) объектов.
Рекомендуется описывать как константные те методы, которые предназначены для получения значений полей.
Указатель this
Каждый объект содержит свой экземпляр полей класса. Методы места в классе не занимают и не дублируются для каждого объекта. Единственный экземпляр метода используется всеми объектами совместно, поэтому нестатический метод должен "знать", для какого объекта он вызван.
Каждый нестатический метод, помимо явно объявленных параметров, получает еще один скрытый параметр: константный указатель на объект, для которого он вызван. В С++ это указатель обозначается зарезервированным словом this. Когда имя параметра метода совпадает с именем поля класса, доступ к полю выполняется через этот указатель (например, this -> num).
Выражение *this представляет собой разыменование указателя и имеет тип определяемого класса. Обычно это выражение возвращается в качестве результата, если метод возвращает ссылку на свой класс ( return *this; ).
Для иллюстрации использования указателя this добавим в приведенный выше класс monster новый метод, возвращающий ссылку на наиболее здорового (поле health ) из двух монстров, один из которых вызывает метод, а другой передается ему в качестве параметра (метод нужно поместить в секцию public описания класса):
monster & the_best(monster &M)
{
if( health > M.get_health())
return *this;
return M;
}
...
monster Vasia(50), Super(200);
// Новый объект Best инициализируется значениями полей Super
monster Best = Vasia.the_best(Super);
Конструкторы
Конструктор предназначен для инициализации объекта и вызывается автоматически при его создании. Ниже перечислены основные свойства конструкторов.
Конструктор не возвращает значения, даже типа void. Нельзя получить указатель на конструктор.
Класс может иметь несколько конструкторов с разными параметрами для разных видов инициализации (при этом используется механизм перегрузки).
Конструктор, который можно вызвать без параметров, называется конструктором по умолчанию.
Параметры конструктора могут иметь любой тип, кроме этого же класса. Можно задавать значения параметров по умолчанию. Их может содержать только один из конструкторов.
Если программист не указал ни одного конструктора, компилятор создает его автоматически (кроме случая, когда класс содержит константы и ссылки, поскольку их необходимо инициализировать). Такой конструктор вызывает конструкторы по умолчанию для полей класса и конструкторы базовых классов.
Конструкторы не наследуются.
Конструктор не может быть константным, статическим и виртуальным (нельзя использовать модификаторы const, virtual и static ).
Конструкторы глобальных объектов вызываются до вызова функции main. Локальные объекты создаются, как только становится активной область их действия. Конструктор запускается и при создании временного объекта (например, при передаче объекта из функции).
При объявлении объектов вызывается один из конструкторов. При отсутствии инициализирующего выражения в объявлении объекта вызывается конструктор по умолчанию, при инициализации другим объектом того же типа - конструктор копирования (см. далее), при инициализации полей - один из явно определенных конструкторов инициализации (т.е. конструкторов, которым передаются параметры для инициализации полей объекта).
Конструкторы часто вызываются неявно для создания временных объектов. Обычно это происходит в следующих случаях:
· при инициализации;
· при выполнении операции присваивания;
· для задания значений параметров по умолчанию;
· при создании и инициализации массива;
· при создании динамических объектов;
· при передаче параметров в функцию и возврате результатов по значению.
Примеры:
monster Super(200, 300), Vasia(50);
monster X = monster(1000);
В последнем операторе создается объект Х, которому присваивается безымянный объект со значением параметра health = 1000 (значения остальных параметров устанавливаются по умолчанию).
При создании динамического массива вызывается конструктор без аргументов.
В качестве примера класса с несколькими конструкторами усовершенствуем описанный ранее класс monster, добавив в него поля, задающие цвет ( skin ) и имя ( name ):
enum color {red, green, blue}; // Возможные значения цвета
class monster
{
int health, ammo;
color skin;
char *name;
public:
monster(int he = 100, int am = 10);
monster(color sk);
monster(char * nam);
...
};
//--------------------------------
monster::monster(int he, int am)
{ health = he; ammo = am; skin = red; name = 0;}
//--------------------------------
monster::monster(color sk)
{
switch (sk)
{
case red: health = 100; ammo = 10; skin = red; name = 0; break;
case green: health = 100;ammo = 20;skin = green;name = 0;break;
case blue: health = 100; ammo = 40; skin = blue;name = 0;break;
}
}
//--------------------------------
monster::monster(char * nam)
{
/* К длине строки добавляется 1 для хранения нуль-символа */
name = new char [strlen(nam) + 1];
strcpy(name, nam);
health = 100; ammo = 10; skin = red;
}
//--------------------------------
monster * m = new monster ("Ork");
monster Green (green);
Первый из приведенных выше конструкторов является конструктором по умолчанию, поскольку его можно вызвать без параметров. Объекты класса monster теперь можно инициализировать различными способами, требуемый конструктор будет вызван в соответствии со списком инициализации. При задании нескольких конструкторов следует соблюдать те же правила, что и при написании перегруженных функций - у компилятора должна быть возможность распознать нужный вариант.
Существует еще один способ инициализации полей в конструкторе (кроме уже описанного присваивания полям значений параметров) - с помощью списка инициализаторов, расположенным после двоеточия между заголовком и телом конструктора:
monster::monster(int he, int am):
health (he), ammo (am), skin (red), name (0){}
Поля перечисляются через запятую. Для каждого поля в скобках указывается инициализирующее значение, которое может быть выражением. Без этого способа не обойтись при инициализации полей-констант, полей-ссылок и полей-объектов. В последнем случае будет вызван конструктор, соответствующий указанным в скобках параметрам.
Наследование
При большом количестве никак не связанных классов управлять ими становится невозможным. Наследование позволяет справиться с этой проблемой путем упорядочивания и ранжирования классов, то есть объединения общих для нескольких классов свойств в одном классе и использования его в качестве базового.
Механизм наследования классов позволяет строить иерархии, в которых производные классы получают элементы родительских, или базовых, классов и могут дополнять их или изменять их свойства.
Классы, находящиеся ближе к началу иерархии, объединяют в себе наиболее общие черты для всех нижележащих классов. По мере продвижения вниз по иерархии классы приобретают все больше конкретных черт. Множественное наследование позволяет одному классу обладать свойствами двух и более родительских классов.
Виды наследования
При описании класса в его заголовке перечисляются все классы, являющиеся для него базовыми. Возможность обращения к элементам этих классов регулируется с помощью модификаторов наследования private, protected и public:
class имя : [private | protected | public] базовый_класс{тело класса};
Если базовых классов несколько, они перечисляются через запятую. Перед каждым может стоять свой модификатор наследования. По умолчанию для классов он private, а для структур - public.
Если задан модификатор наследования public, оно называется открытым. Использование модификатора protected делает наследование защищенным, а модификатора private - закрытым. Это не просто названия: в зависимости от вида наследования классы ведут себя по-разному. Класс может наследовать от структуры, и наоборот.
До сих пор мы рассматривали только спецификаторы доступа private и public, применяемые к элементам класса. Для любого элемента класса может также использоваться спецификатор protected, который для одиночных классов, не входящих в иерархию, равносилен private. Разница между ними проявляется при наследовании.
Private элементы базового класса в производном классе недоступны вне зависимости от ключа. Обращение к ним может осуществляться только через методы базового класса.
Элементы protected при наследовании с ключом private становятся в производном классе private, в остальных случаях права доступа к ним не изменяются.
Доступ к элементам public при наследовании становится соответствующим ключу доступа.
Если базовый класс наследуется с ключом private, можно выборочно сделать некоторые его элементы доступными в производном классе, объявив их в секции public производного класса с помощью операции доступа к области видимости:
class Base{...
public: void f();};
class Derived : private Base{...
public: Base::void f();};
Простое наследование
Простым называется наследование, при котором производный класс имеет одного родителя. Для различных элементов класса существуют разные правила наследования. Рассмотрим наследование классов на примере.
Создадим производный от класса monster класс daemon, добавив полезную в некоторых случаях способность думать:
enum color {red, green, blue};
// ------------- Класс monster -------------
class monster
{
// ------------- Скрытые поля класса:
int health, ammo;
color skin;
char *name;
public:
// ------------- Конструкторы:
monster(int he = 100, int am = 10);
monster(color sk);
monster(char * nam);
monster(monster &M);
// ------------- Деструктор:
~monster() {delete [] name;}
// ------------- Операции:
monster& operator ++(){++health; return *this;}
monster operator ++(int)
{monster M(*this); health++; return M;}
operator int(){return health;}
bool operator >(monster &M)
{
if( health > M.get_health()) return true;
return false;
}
monster& operator = (monster &M)
{
if (&M == this) return *this;
if (name) delete [] name;
if (M.name)
{
name = new char [strlen(M.name) + 1];
strcpy(name, M.name);
}
else name = 0;
health = M.health; ammo = M.ammo; skin = M.skin;
return *this;
}
// ------------- Методы доступа к полям:
int get_health() const {return health;}
int get_ammo() const {return ammo;}
// ------------- Методы, изменяющие значения полей:
void set_health(int he){ health = he;}
void draw(int x, int y, int scale, int position);
};
// ------------- Реализация класса monster -------------
monster::monster(int he, int am):
health (he), ammo (am), skin (red), name (0){}
monster::monster(monster &M)
{
if (M.name)
{
name = new char [strlen(M.name) + 1];
strcpy(name, M.name);
}
else name = 0;
health = M.health; ammo = M.ammo; skin = M.skin;
}
monster::monster(color sk)
{
switch (sk)
{
case red:health = 100; ammo = 10; skin = red; name = 0; break;
case green:health = 100;ammo = 20;skin = green; name = 0; break;
case blue: health = 100; ammo = 40; skin = blue; name = 0;break;
}
}
monster::monster(char * nam)
{
name = new char [strlen(nam)+1];
strcpy(name, nam);
health = 100; ammo = 10; skin = red;
}
void monster::draw(int x, int y, int scale, int position)
{ /* ... Отрисовка monster */ }
// ------------- Класс daemon -------------
class daemon : public monster
{ int brain;
public:
// ------------- Конструкторы:
daemon(int br = 10){brain = br;};
daemon(color sk) : monster (sk) {brain = 10;}
daemon(char * nam) : monster (nam) {brain = 10;}
daemon(daemon &M) : monster (M) {brain = M.brain;}
// ------------- Операции:
daemon& operator = (daemon &M)
{
if (&M == this) return *this;
brain = M.brain;
monster::operator = (M);
return *this;
}
// ------------- Методы, изменяющие значения полей:
void draw(int x, int y, int scale, int position);
void think();};
// ------------- Реализация класса daemon -------------
void daemon::draw(int x, int y, int scale, int position)
{ /* ... Отрисовка daemon */ }
void daemon:: think(){ /* ... */ }
В классе daemon введено поле brain и метод think, определены собственные конструкторы и операция присваивания, а также переопределен метод отрисовки draw. Все поля класса monster, операции (кроме присваивания) и методы get_health, get_ammo и set_health наследуются в классе daemon, а деструктор формируется по умолчанию.
Лекция 7. Операционные системы и их интерфейсы