Лекция 4:

к оглавлению
оглавление

Объектно-ориентированные расширения С++

Рассмотрев свойства С++, которыми можно пользоваться, не отказываясь от традиционного подхода к программированию, обратимся к объектно-ориентированным методам программирования. Прежде всего, вспомним три основные свойства, при наличии которых можно говорить, что некоторый язык предназначен для объектно-ориентированного программирования: ИНКАПСУЛЯЦИЯ, ПОЛИМОРФИЗМ и НАСЛЕДОВАНИЕ.

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

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

Вариант на стандартном С Вариант на С++
#include "stdio.h"
int i;
char temp[80], buff[80];

printf ("Введите число:");
gets(temp); i = atoi(temp);
printf ("Введите символьную строку:");
gets (buff);
printf ("Вы ввели число: %d\n", i);
printf ("Вы ввели строку: %s\n", buff);

#include "iostream.h"
int i;
char buff[80];

cout << "Введите число:";
cin >> i;
cout << "Введите символьную строку:";
cin >> buff;
cout << "Вы ввели число:" << i << "\n";
cout << "Вы ввели строку:" << buff << "\n";

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

Консольный ввод и вывод в С++

Хотяв С++ по-прежнему доступны функции ввода - вывода printf() и scanf() из языка С, С++ обеспечивает иной, лучший способ выполнения этих операций. В С++ ввод/вывод выполняется с использование переопределенных операций << и>>, а не с помощью функций. Сохраняя свои первоначальные значение (левый и правый сдвиг), операции << и >> выполняют еще ввод и вывод.

Для вывода на экран применяется следующая процедура

cout << выражение;

Таким способом можно вывести любой базовый тип данных С++.

Аналогичным образом можно осуществлять и ввод (вместо scanf()).

Например,

int i;
cin >> i;

Обратите внимание, что переменной i не должен предшествовать &. В общем случае, для ввода с клавиатуры следует использовать следующую форму >>:

cin >> выражение;

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

[к началу]

Введение в классы

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

Синтаксис описания класса похож на синтаксис описания структуры.

class имя_класса
{
 закрытые элементы – члены класса
public:
 открытые элементы – члены класса
};

На что здесь следует обратить внимание?

  1. Имя_класса с этого момента становится новым именем типа данных, которое используется для объявления объектов класса.
  2. Члены класса – это переменные состояния и методы этого класса, иными словами членами класса могут быть как переменные, так и функции. Функции и переменные, объявленные внутри объявления класса, становятся членами этого класса. Функции-члены класса будем называть методами этого класса.
  3. По умолчанию, все функции и переменные, объявленные в классе, становятся закрытыми (private) . Т.е. они доступны только из других членов этого класса. Для объявления открытых членов класса используется ключевое слово public. Все функции–методы и переменные, объявленные после слова public, доступны и для других членов класса, и для любой другой части программы, в которой содержится класс. В структурах по умолчанию все члены являются отрытыми. Так что, приведенные ниже примеры аналогичны.

struct _3d
{
 double mod ();
 double projection (_3d r);
private:
 double x, y, z;
};

или

class _3d
{
 double x, y, z;
public:
 double mod ();
 double projection (_3d r);
};

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

Хотя функции mod() и projection(_3d r) объявлены в _3d, они еще не определены. Для определения метода – члена класса, нужно связать имя класса, частью которого является метод, с именем класса. Это достигается путем написания имени функции вслед за именем класса с двумя двоеточиями. Два двоеточия называют операцией расширения области видимости.

double _3d::mod ()
{
 return sqrt (x*x + y*y +z*z);
}

double _3d::projection (_3d r)
{
 return (x*r.x + y*r.y + z*r.z) / mod();
}

...

main()
{
 _3d a, b;
 double dPro, dMod;
 dMod = a.mod();
 dPro = b.projection(a);
}

[к началу]

Перегружаемые функции и операторы (overload)

Одна из ключевых черт полиморфизма в С++ - замещение или перегрузка операторов и функций.

Важным расширением, пришедшим из языка АДА, является то, что транслятор С++ различает функции не только по именам, но и по типу аргументов.

double power (double x) {return x*x;}
int power (int x) {return x*x;}

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

int abs (int x) { return x<0 ? -x:x; }  // 1
long abs (long x) { return x<0 ? -x:x; } // 2
double abs (double x) { return x<0 ? -x:x; } // 3

main()
{
cout << "абсолютная величина -1000" << abs(-1000) << "\n";   // вызов функции 1
cout << "абсолютная величина -1000L" << abs(-1000L) << "\n";   // вызов функции 2
cout << "абсолютная величина -1000.0" << abs(-1000.0) << "\n";   // вызов функции 3
}

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

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

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

Замечание: транслятор не различает функции по типу возвращаемого значения.

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

int i1, i2, i3;
long int l1, l2, l3;
double f1, f2, f3;
i1 = i2 + i3;
l1 = l2 + l3;
f1 = f2 + f3;

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

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

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

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

struct _3d
{
 double x, y, z; // координаты
 _3d operator + (_3d);
};
_3d _3d::operator + (_3d b)
{
 _3d c;
 c.x = x + b.x;
 c.y = y + b.y;
 c.z = z + b.z;
 return c;
}

Мы включили описание прототипа оператора внутрь структуры (инкапсулировали). Определение (реализацию) кода вынесем. Доступ к элементам структуры осуществляется привычным образом - через ".".

Теперь мы вправе написать следующий фрагмент:

_3d A, B, C;
A.x = 1.0;
A.y = 1.0;
A.x = 1.0;
B = A;
C = A + B; // это эквивалентно вызову C = A.operator + (B); /operator +(...) – один из методов объекта A/

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

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

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

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

Например,

void f (int i=0, int j=1); // прототип функции f (...)
...
void f (int i, int j)
{
... // тело функции
}

Теперь эту функцию можно вызывать тремя способами

f (); // i по умолчанию равно 0, j по умолчанию равно 1
f (10); // i равно 10, j по умолчанию равно 1
f (11, 22); // i равно 11, j равно 22

Задать значение j, установив i по умолчанию, нельзя. И вообще, все параметры по умолчанию должны находиться правее аргументов, передаваемых обычным путем.

Несколько слов об аргументах по умолчанию. Ими могут быть только константы или глобальные переменные.

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

[к началу]
назад КОНЕЦ ЧЕТВЕРТОЙ СЕРИИ вперед