ТОП 10:

При множественном наследовании



Рассмотрим следующую ситуацию.
Vnuk
Child2
Child1  
Parent

Имеется базовый класс Parent, есть два порождённых класса Child1, Child2 и есть класс Vnuk, порождённый одновременно классами Child1 и Child2 (см. рис.). В такой ситуации проблемы могут возникнуть, если метод класса Vnuk захочет получить доступ к данным или методам класса Parent.

class Parent {

protected:

int basedata;

};

class Child1 : public Parent

{ };

class Child2 : public Parent

{ };

class Vnuk : public Child1, public Child2 {

public:

int getdata()

{ return basedata; } // ОШИБКА: неоднозначность

};

Здесь, при попытке метода int getdata() получить доступ к переменной базового класса basedata возникнет ошибка. Дело в том, что каждый из порождённых классов (Child1 и Child2) наследует свою копию базового класса Parent. Это копия называется подобъектом. Каждый из двух подобъектов содержит собственную копию данных базового класса, включая basedata. Здесь и возникает неоднозначная ситуация. Для устранения того рода неоднозначности классы Child1 и Child2 необходимо сделать наследниками виртуального базового класса, что приведет к созданию только одной общей копии базового класса Parent.

… тоже самое

class Child1 : virtual public Parent

{ };

class Child2 : virtual public Parent

… тоже самое

Приведём более конкретный пример.

class A {

protected:

int x;

public:

A() { x=0; }

A(int xx) { x=xx; }

};

//---------------------------------------------------------------------------

class B : virtual public A {

public:

void addB(int y) { x=x+y; }

};

//---------------------------------------------------------------------------

class C : virtual public A {

public:

void addC(int y) { x=x*y; }

};

//---------------------------------------------------------------------------

class Vnuk : public B, public C {

public:

void show() { cout << "X= " << x << endl; } //если-бы не virtual, то здесь возникла бы неоднозначность

};

//---------------------------------------------------------------------------

int main() {

Vnuk v1;

v1.show(); // 0

v1.addB(10); v1.show(); // 10

v1.addC(5); v1.show(); // 50

getch(); return 0;

}

Добавив спецификатор virtual перед модификаторами доступа к базовому классу А в объявлениях классов В и С, мы включили в класс Vnuk только одну копию класса А, тем самым устранили проблему неоднозначности.

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

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

class Int {

private:

int i;

public:

Int() { i=0; }

Int(int ii) { i=ii; }

void show() { cout << i; }

friend Int sum(Int a1, Int a2); // это - не метод, это – дружественная функция

};

//---------------------------------------------------------------------------

int main() {

Int i1(3), i2(7), i3;

i3 = sum(i1, i2);

i3.show();

getch(); return 0;

}

//---------------------------------------------------------------------------

Int sum(Int a1, Int a2) { //классическое определение функции

return (a1.i + a2.i);

}

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

class Beta; //нужно для объявления дружественной func

 

class Alpha {

private:

int data;

public:

Alpha() : data(3) { } //конструктор без аргументов

friend int func(Alpha, Beta); //дружественная функция

};

//---------------------------------------------------------------------------

class Beta {

private:

int data;

public:

Beta() : data(7) { } //конструктор без аргументов

friend int func(Alpha, Beta); //дружественная функция

};

//---------------------------------------------------------------------------

int func(Alpha a, Beta b) //определение функции

{ return( a.data + b.data ); }

//---------------------------------------------------------

int main() {

Alpha aa;

Beta bb;

cout << func(aa, bb) << endl; //вызов функции, увидим 10

getch(); return 0;

}

В этой программе функция int func(Alpha a, Beta b) объявлена в двух классах с ключевым словом friend , что позволяет объяснить компилятору, что она является дружественной, т.е. имеет доступ к скрытым данным двух классов. Объект каждого класса передаётся как параметр функции func(Alpha, Beta), и функция имеет доступ к скрытым данным обоих классов посредством этих аргументов.

Из-за того, что к классу нельзя обращаться до того, как он объявлен в программе, здесь в начале программы мы объявили класс Beta до класса Alpha с помощью следующего оператора: class Beta; .

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

Дружественные классы

Методы могут быть превращены в дружественные функции одновременно с определением всего класса как дружественного.

class Alpha {

private:

int data1;

public:

Alpha() : data1(99) { } //конструктор

friend class Beta; //beta – дружественный класс

};

//---------------------------------------------------------------------------

class Beta { //все методы имеют доступ

public: //к скрытым данным alpha

void func1(Alpha a) { cout << "\ndata1=" << a.data1;}

void func2(Alpha a) { cout << "\ndata1=" << a.data1;}

};

//---------------------------------------------------------------------------

int main() {

Alpha a;

Beta b;

 

b.func1(a);

b.func2(a);

getch(); return 0;

}

В данной программе весь класс Beta провозглашён дружественным для класса Alpha, посредством выражения:friend class Beta;. Это даёт возможность всем методам класса Beta получить доступ к скрытым данным класса Alpha.

Указатель this

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

class Gde {

private:

char charray[10]; //массив из 10 байтов

public:

void adres() { cout << "\nMoi adres - " << this; } // вывести адрес объекта

};

//---------------------------------------------------------------------------

int main() {

Gde w1, w2, w3; //создать три объекта

w1.adres(); w2.adres(); w3.adres(); //посмотреть, где они находятся

cout << endl; getch(); return 0;

}

Здесь, с помощью метода adres() выводятся адреса объектов. Этот метод просто выводит значение указателя this. Так как данные объектов хранятся в массивах, размер каждого из которых 10 байтов, то объекты в памяти будут отделены друг от друга десятью байтами (может 12).

Когда вызывается какой-либо метод, значением указателя this становится адрес объекта, для которого этот метод вызван. Указатель this можно использовать для получения доступа к данным объекта, на который он ссылается.

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

class Alpha {

private:

int data;

public:

Alpha() // конструктор без аргументов

{ }

Alpha(int d) // конструктор с одним аргументом

{ data = d; }

void display() // вывести данные

{ cout << data; }

Alpha& operator= (Alpha& a) { // перегружаемая операция =

data = a.data; // конвертируем тип Alpha в int

cout << "\nPeregr. operator =";

return *this; // вернуть копию this Alpha

}

};

//---------------------------------------------------------------------------

int main() {

Alpha a1(37);

Alpha a2, a3;

a3 = a2 = a1; // перегружаемый =, дважды

cout << "\na2="; a2.display(); // вывести a2 (увидим 37)

cout << "\na3="; a3.display(); // вывести a3 (увидим 37)

cout << endl; getch(); return 0;

}

Так как this является указателем на объект, чей метод выполняется, то *this – это и есть сам объект, и выражение return *this; возвращает его по ссылке. Таким образом мы избежали создания никому не нужных дополнительных копий объекта, которые бы создались, если бы мы использовали возврат по значению (Alpha operator= (Alpha& a)).

 

МНОГОФАЙЛОВЫЕ ПРОГРАММЫ

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

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

– общедоступного компонента, называемого интерфейсом (interface) и содержащего объявления классов в заголовочном файле (*.h);

– скрытого компонента, называемого реализацией (implementation) и содержащего определения методов в объектном (*.obj) или библиотечном (*.lib) файле.

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

Рассмотрим пример многофайловой программы. Элементарная многофайловая программа обычно состоит из трёх частей:

1) заголовочного файла, называемого интерфейсной частью программы и хранящего определение класса с объявленными, но не определёнными методами;

2) файла реализации, называемого реализационной частью программы и хранящего определения методов;

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

// Заголовочный файл interface.h

#include <iostream>

#include <conio>

using namespace std;

//---------------------------------------------------------------------------

class Int {

private:

int i;

public:

Int() { i=0; }

Int(int d) { i=d; }

void show();

Int operator+(Int& b);

};

//---------------------------------------------------------------------------

 

// Файл реализации realizacia.cpp

#include "interface.h"

//---------------------------------------------------------------------------

void Int::show() {

cout << i;

}

//---------------------------------------------------------------------------

Int Int::operator+(Int& b) {

return (i+b.i);

}

//---------------------------------------------------------------------------

 

// Файл прикладной программы programm.cpp

#include "interface.h"

//---------------------------------------------------------------------------

int main() {

Int i1(7), i2(3), res;

res = i1 + i2; cout << "Summa = "; res.show();

getch(); return 0;

}

//---------------------------------------------------------------------------

 

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

1) Создаём модуль для прикладной программы: File/New/Other/Console Wizard (þ C++; þ Console Application). Созданный таким образом Unit.cpp сохраняем как programm.cpp в своей папке.

2) Создаём заголовочный файл: File/New/Other/Header File. Созданный таким образом File1.h сохраняем сразу в своей папке как interface.h .

3) Создаём файл реализации: File/New/Other/Cpp File и сохраняем его сразу как realizacia.cpp

4) Заполняем соответствующим кодом созданные файлы (см. выше), затем сохраняем Save All как, например, mnogoF.bpr. Необходимо помнить, что в файлах реализации и прикладной программы нужно включать написанный вами заголовочный файл (#include "interface.h" ).

5) Осуществляем компоновку(Project/Build *.bpr) и компиляцию проекта.

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

 

 







Последнее изменение этой страницы: 2016-04-08; Нарушение авторского права страницы

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.231.167.166 (0.017 с.)