Основы объектно-ориентированного программирования СИНТАКСИС ОСНОВНЫХ КОНСТРУКЦИЙ

Объявление базовых классов

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

Базовый класс

Базовый класс определяется следующим образом (синтаксис):

class className

{

private:

<закрытые элементы-данные>

<закрытые конструкторы>

<закрытые функции-элементы>

protected:

<защищенные элементы-данные>

<защищенные конструкторы>

<защищенные функции-элементы>

public:

<открытые элементы-данные>

<открытые конструкторы>

<открытый деструктор>

<открытые функции-элементы>

};

Пример 1:

class point

{

protected:

double х;

double у;

public:

point(double xVal, double yVal);

double getX();

double getY();

void assign(double xVal, double yVal);

point& assign(point &pt);

};

Разделы класса

Классы С++ имеют три различных уровня доступа к своим элементам - как к данным, так и к функциям:

- Закрытые (частные) элементы

- Защищенные элементы

- Открытые элементы

К данным в закрытом разделе имеют доступ только функции-элементы класса.

Классам-потомкам запрещен доступ к закрытым данным своих 6азовых классов.

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

Существуют следующие правила для разделов класса:

1. Разделы могут появляться в любом порядке.

2. Один и тот же раздел можно определять несколько раз.

3. Если не определен ни один раздел, компилятор (по умолчанию) объявляет все элементы закрытыми.

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

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

6. Класс может иметь несколько конструкторов.

7. Класс может иметь только один деструктор, который должен объявляться в открытом разделе класса.

8. Функции-элементы (в том числе конструкторы и деструкторы), состоящие из нескольких операторов, должны определяться вне объявления класса. Определение функции может содержаться в том же файле, в котором определяется класс. Это напоминает порядок работы с обычными функциями: задание прототипа и определение функции.

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

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

class point

{

protected:

double x;

double y;

public:

point(double xVal, double yVal);

double getX();

// другие функции-элементы

};

Определения конструктора и функций-элементов должны выглядеть так

point::point (double xVal, double yVal)

{

// операторы

}

double point::getX()

{

// операторы

}

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

В листинге8.1 приведен исходный текст программы RECT.CPP. Программа предлагает вам ввести длину и ширину прямоугольника (в данном примере прямоугольник является объектом). Затем программа выводит значения длины, ширины и площади определенного вами прямоугольника.

Конструкторы

Конструкторы и деструкторы в С++ вызываются автоматически, что гарантирует правильное создание и разрушение объектов класса.

Общий вид (синтаксис) объявления конструктора:

class className

{

public:

className(); // конструктор по умолчанию

className(const className &c); // конструктор копии

className(<список параметров>); // остальные конструкторы

};

Пример 2:

// Замечание: Здесь только объявление класса без описания объявленных

// функций-параметров

class point

{

protected:

double x;

double y;

public:

point();

point(double xVal, double yVal);

point(const point &pt);

double getX();

double getY();

void assign(double xVal, double yVal);

point& assign(point &pt);

};

int main()

{

point p1;

point p2(10, 20);

point p3(p2);

p1.assign(p2);

cout << p1.getX() << " " << p1.getY() << endl;

cout << p2.getX() << " " << p2.getY() << endl;

cout << p3.getX() << " " << p3.getY() << endl;

return 0;

}

Конструктор копии создает объект класса, копируя при этом данные из существующего объекта класса.

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

1. Имя конструктора класса должно совпадать с именем класса.

2. Нельзя определять тип возвращаемого значения для конструктора, даже тип void.

3. Класс может иметь несколько конструкторов или не иметь их совсем.

4. Конструктором по умолчанию является конструктор, не имеющий параметров, или конструктор, у которого все параметры имеют значения по умолчанию.

Рассмотрим два примера с фрагментами объявления конструкторов.

// класс с конструктором без параметров

class point1

{

protected:

double x;

double y;

public:

point1();

// другие функции-элементы

};

// конструктор класса имеет параметры со значениями по умолчанию

class point2

{

protected:

double x;

double y;

public:

point2(double xVal = 0, double yVal = 0);

// другие функции-элементы

};

5. Конструктор копии создает объект класса на основе существующего объекта.

Например:

class point

{

protected:

double x;

double y;

public:

point();

point(double xVal = 0, double yVal = 0);

point(const point &pt);

// другие функции-элементы

};

6. Объявление объекта класса, которое может содержать параметры и, в качестве параметра, имя уже существующего объекта, влечет за собой вызов конструктора. Но какой из конструкторов будет использоваться в каждом конкретном случае? Ответ зависит от того, как много конструкторов вы объявили и с какими аргументами вы объявляете объект класса. Например, рассмотрим следующие объявления объектов последней версии класса point:

point p1; // применяется конструктор по умолчанию

point p2(1.1, 1.3); // используется второй по счету конструктор

point p3(p2); // используется конструктор копии

Поскольку объект p1 объявляется без параметров, компилятор использует

конструктор по умолчанию. Объект p2 объявляется с двумя вещественными

аргументами, поэтому компилятор вызовет второй конструктор. Объект p3

при объявлении имеет параметром объект p2, поэтому компилятор вызовет

конструктор копии, чтобы создать новый объект из объекта p2.

ВНИМАНИЕ:

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

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

данные-указатели.

Деструкторы

Классы С++ могут содержать деструкторы, которые автоматически разрушают объекты класса.

Общий синтаксис объявления деструктора:

class className

{

public:

className(); // конструктор по умолчанию

// другие конструкторы

~className(); // объявление деструктора

// другие функции-элементы

};

Пример 3 на синтаксис обявления деструктора:

class String

{

protected:

char *str;

int len;

public:

String();

String(const String& s);

~String();

// другие функции-элементы

};

Деструкторы в С++ имеют следующие особенности и подчиняются следующим правилам:

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

2. Нельзя определять тип возвращаемого значения, даже тип void.

3. Класс может иметь только один деструктор или ни одного. В последнем случае компилятор создаст деструктор по умолчанию.

4. Деструктор не должен иметь параметров.

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

(см. LIST8-2.CPP)

Объявление иерархии классов

Производный класс

Общая форма (синтаксис) объявления производного класса:

class classname : [<спецификатор доступа>] parentClass

{

<дружественные классы>

private:

<закрытые элементы-данные>

<закрытые конструкторы>

<закрытые функции-элементы>

protected:

<защищенные элементы-данные>

<защищенные конструкторы>

<защищенные функции-элементы>

public:

<открытые элементы-данные>

<открытые конструкторы>

<открытый деструктор>

<открытые функции-элементы>

<дружественные функции и дружественные операции>

};

Пример 4 объявления класса Rectangle и класса-потомка Box:

class Rectangle

{

protected:

double length;

double width;

public:

Rectangle(double len, double wide);

double getLength() const;

double getWidth() const;

double assign(double len, double wide);

double calcArea();

};

class Вох : public Rectangle

{

protected:

double height;

public:

Box(double len, double wide, double height);

double getHeight () const;

assign(double len, double wide, double height);

double calcVolume();

};

(см. LIST8-3.CPP)

Виртуальные функции

Мы уже упоминали о полиморфизме - важной особенности объектно-

ориентированного программирования. Рассмотрим следующий пример (6):

#include <iostream.h>

class X

{

public:

double A(double x) { return x * x; }

double B(double x) { return A(x) / 2; }

};

class Y : public X

{

public:

double A(double x) { return x * x * x; }

};

int main ()

{

Y y;

cout << y.B(3) << endl;

return 0;

}

В классе X объявляются функции A и B, причем функция B вызывает функцию А. Класс Y, потомок класса X, наследует функцию B, но переопределяет функцию A. Цель этого примера - демонстрация полиморфного поведения класса Y. Мы должны получить следующий результат: вызов наследуемой функции X::B должен привести к вызову функции Y::A. Что же выдаст нам наша программа? Ответом будет 4.5, а не 13.5! В чем же дело? Почему компилятор разрешил выражение y.B(3) как вызов наследуемой функции X::B, которая, в свою очередь, вызывает X::A, а не функцию Y::A, что должно было бы произойти в случае полиморфной реакции класса?

Виртуальные функции объявляются следующим образом (синтаксис):

class className1

{

// функции-элементы

virtual returnType functionName(<список параметров>);

};

class className2 : public className1

{

// функции-элементы

virtual returnType functionName(<список параметров>);

};

Пример 7, показывающий, как при помощи виртуальных функций можно реализовать полиморфное поведение классов X и Y:

#include <iostream.h>

class X

{

public:

virtual double A(double x) { return x * x; }

double B (double x) { return A(x) / 2; }

};

class Y : public X

{

public:

virtual double A(double x) { return x * x * x; }

};

main()

{

Y y;

cout << y.B(3) << endl;

return 0;

}

Этот пример выведет вам правильное значение 13.5, потому что в результате вызова наследуемой функции X::B, вызывающей функцию A, в качестве функции A во время выполнения программы будет использована замещающая функция Y::A.

*** Правило виртуальной функции ***

Правило виртуальной функции гласит:

"Виртуальная однажды - виртуальна всегда".

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

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

#include <iostream.h>

class A

{

public:

A() {}

virtual void foo(char c)

{ cout << "virtual A::foo() returns " << c << endl; }

};

class B : public A

{

public:

B() {}

void foo(const char* s)

{ cout << "B::foo() returns " << s << endl; }

void foo(int i)

{ cout << "B::foo() retuzns " << i << endl; }

virtual void foo(char c)

{ cout << "virtual B::foo() returns " << c << endl; }

};

class C: public B

{

public:

C() {}

void foo(const char* s)

{ cout << "C::foo() returns " << s << endl; }

void foo(double x)

{ cout << "C::foo() returns " << x << endl; }

virtual void foo(char c)

{ cout << "virtual C::foo() returns " << c << endl; }

};

int main()

{

A Aobj;

B Bobj;

C Cobj;

Aobj.foo('A');

Bobj.foo('B');

Bobj.foo(10);

Bobj.foo("Bobj");

Cobj.foo('C');

Cobj.foo(144.123);

Cobj.foo("Cobj");

return 0;

}

В этом примере вводятся три класса - A, B и C - образующих линейную иерархию наследования. В классе A объявляется виртуальная функция foo(char).

Класс B объявляет свою версию виртуальной функции foo(char), но, кроме того, в классе B объявляются невиртуальные перегруженные функции foo(const char*) и foo(int). Класс C объявляет свою версию виртуальной функции foo(char) и невиртуальные перегруженные функции foo(const char*) и foo(double). Обратите внимание на то, что в классе C приходится заново объявлять функцию foo(const char*), поскольку в данном случае функция-элемент B::foo(const char*) не наследуется. Таким образом, в С++ схема наследования отличается от обычной для случая виртуальной и перегруженных функций с одинаковым именем. В функции main объявляются объекты для всех трех классов и вызываются различные версии функции-элемента foo.

Дружественные функции

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

Общий вид (синтаксис) объявления дружественной функции следующий:

class className

{

public:

className();

// другие конструкторы

friend returnType friendFunction(<список параметров>);

};

Пример 9:

class String

{

protected:

char *str;

int len;

public:

String();

~String();

// другие функции-элементы

friend String& append(String &str1, String &str2);

friend String& append(const char* str1, String &str2);

friend String& append(String &str1, const char* str2);

};

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

функций-элементов решаются с трудом, неуклюже или не могут быть решены вообще.

Рассмотрим простой пример использования дружественных функций. Текст программы FRIEND.CPP представлен в листинге 8.5. Программа следит за памятью, отведенной для хранения массива символов. Эта программа - первый шаг к созданию класса string.