Управление уровнем доступа к членам класса



Мы поможем в написании ваших работ!


Мы поможем в написании ваших работ!



Мы поможем в написании ваших работ!


ЗНАЕТЕ ЛИ ВЫ?

Управление уровнем доступа к членам класса



В предыдущих примерах базовый класс являлся общим базовым классом для производного класса:

class Derived: public Base{…};

Это означает, что уровень доступа к членам класса Base для функций-членов класса Derived и просто пользователей класса Derived остался неизменным: личные члены класса Base не доступны в классе Derived, общие и защищенные члены класса Base остались соответственно общими и защищенными в Derived. Если не указать, что базовый класс является общим, то по умолчанию он будет личным:

class Derived: Base{…}; // Base - личный базовый класс.

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

Базовый класс не может быть защищенным базовым классом.

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

class Base {

private: int privm;

protected: int protm;

public: int pubm;

};

class Derived: Base{// Личный базовый класс.

public:

Base::pubm; // Теперь pubm - общий член класса Derived;

Base::protm; // ошибка - изменение уровня доступа.

protected:

Base::protm; //Теперь protm - защищенный член класса Derived;

Base::pubm; // ошибка - изменение уровня доступа.

Структуры могут использоваться подобно классам, но с одной особенностью. Если производным классом является структура, то ее базовый класс всегда является общим базовым классом, т.е. объявление вида:

struct B: A {…};

Эквивалентно

class B: public A{…};

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

struct B: A{…};

эквивалентна

сlass B: public A {public: …};

19.4. Последовательность вызова конструктора и деструктора
при построении производного класса на основе одного базового

Объект производного класса в качестве данных-членов класса, может содержать объекты абстрактных типов.

class string{. . .

public:

string (char*);

~string ();

};

class Base{…

public:

Base (int);

~Base ();

. . .

};

class Derived: public Base {

Base b;

string s;

public:

Derived (char*, int);

~Derived ();

. . .

};

Перед обращением к собственно конструктору класса Derived необходимо, во-первых, создать подобъект типа Base, во- вторых, члены b и s. Поскольку для их создания нужно обратиться к конструкторам соответствующих классов, мы должны им всем передать необходимые списки аргументов:

Derived::Derived (char *st, int len): Base (len), b (len+1), s (str) {…}

В этом случае при создании объекта типа Derived сначала будет создан подобъект типа Base. При этом будет вызван конструктор Base::Base() с аргументом len. Затем будут созданы объекты b и s в том порядке, в котором они указаны в определении класса Derived. После этого будет выполнен конструктор Derived::Derived (). Деструкторы будут вызваны в обратном порядке.

 

28. Преобразования типов. Связь с наследованием.

Преобразование типов

Преобразование типов можно разделить на 4 группы:

1) Стандартный к стандартному;

2) стандартный к абстрактному;

3) абстрактный к стандартному;

4) абстрактный к абстрактному.

Первые преобразования уже были нами рассмотрены. Преобразования второй группы основаны на использовании конструкторов, как явном, так и неявном.

Снова рассмотрим касс complex:

class complex {

double re, im;

public: complex (double r = 0, double i = 0){ re = r; im = i ; }

. . .

};

Объявление вида

complex c1;

complex c2 (1.8);

complex c3 (1.2, 3.7);

обеспечивают создание комплексных чисел.

Но конструктор может вызываться и неявно, в том случае, когда в выражении должен находиться операнд типа complex, а на самом деле присутствует операнд типа double:

complex operator + (complex & op, complex & op2 );

complex operator – (complex & op, complex & op2 );

complex operator * (complex & op, complex & op2 );

complex operator / (complex & op, complex & op2 );

complex operator – (complex & op ); // Унарный минус.

complex res;

res = – (c1 + 2) * c2 / 3 + .5 * c3;

Интерпретация, например, выражения -– (c1 + 2) будет следующей:

operator – (( operator + ( c1, complex ( double (2)))).

При выполнении этого выражения неявные вызовы конструкторов создадут временные константы типа complex: (2.0, 0.0), (3.0, 0.0), (4.5, 0.0), которые будут уничтожены сразу же после того, как в них отпадет надобность. Заметим, что здесь не только происходит неявный вызов конструктора complex, но и неявное стандартное преобразование значения типа int к типу double. Число уровней неявных преобразований ограничено. При этом правила таковы: компилятор может выполнить не более одного неявного стандартного преобразования и не более одного неявного преобразования, определенного программистом.

Пример:

сlass A{ public:

A (double d){. . .}

};

class B{ public:

B (A va){. . .}

};

class C{ public:

C (B vb){ . . .}

};

A var1 (1.2); // A(double)

B var2 (3.4); // B(A(double))

B var3 (var1); // B(A)

C var4 (var3); // C(B)

C var5 (var1); // C(B(A))

C var6 (5.6); // Ошибка! Неявно вызывается C(B(A(double)))

C var7 (A(5.6)); // C(B(A))

Ошибка при создании переменной var6 связана с тем, что необходимо два уровня неявных нестандартных преобразований, выполняющихся с помощью вызова конструкторов: double к А, а затем A к В.

При создании переменной var7 одно из этих преобразований - double к А - сделано явным, и теперь все будет нормально.

Таким образом, конструктор с одним аргументом Class::Class(type) всегда определяет преобразование типа type к типу Class, а не только способ создания объекта при явном обращении к нему.

Для преобразования абстрактного типа к стандартному или абстрактного к абстрактному в С++ существует средство – функция, выполняющая преобразование типов, или оператор–функция преобразования типов.

Она имеет вид

Class::operator type (void);

Эта функция выполняет определенное пользователем преобразование типа Class к типу type. Эта функция должна быть членом класса Class и не иметь аргументов. Кроме того, в ее объявлении не указывается тип возвращаемого значения. Обращение к этой функции может быть как явным, так и неявным. Для выполнения явного преобразования можно использовать как традиционную, так и «функциональную» форму.

Пример 1:

class X { int a, b;

public:

X (X & vx) { a = vx.a; b = vx.b; }

Х (int i, int j) { a = 2*i, b = 3*j; }

operator double () { return (a + b)/2.0; } // Преобразование

// абстрактного типа к стандартному. };

int i = 5;

double d1 = double (i); // Явное преобразование типа int к double;

double d2 = i ; // неявное преобразование int к double;

X xv (5, -8);

double d3 = double (xv); // явное преобразование типа Х к double;

double d4 = xv; // неявное преобразование Х к double.

Пример 2:

// Преобразование абстрактного типа к абстрактному.

class Y {

char * str1; // Строки str1 и str2 хранят символьное

char * str2; // представление целых чисел.

public:

Y (char *s1, char *s2): str1(s1), str2 (s2){}

operator X() { return X (atoi (str1), atoi (str2));}

};

. . .

Y yvar (“12“,“-25“);

X xvar = yvar;

При создании переменной xvar перед вызовом конструктора копирования X::X(X&) будет выполнено неявное преобразование значения переменной yvar к типу Х. Это же преобразование в явном виде может выглядеть так:

X xvar = X (yvar);

X xvar = (X) yvar;

Для выражения

X xvar = X (“12“, “-25“);

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


Раннее и позднее связывание (полиморфизм). Виртуальные функции.

Полиморфизм

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

Раннее и позднее связывание

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

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

В таком подходе надо описать базовый класс Employee, а затем со­здать производные от него классы для всех форм оплаты. Каждый производный класс будет иметь собственную реализацию метода CalculatePay().

Другой пример: базовый класс figure может описывать фигуру на экране без конкретизации её вида, а производные классы triangle (треугольник), ellipse (эллипс) и т.д. однозначно определяют её форму и размер. Если в базовом классе ввести функцию void show () для изображения фигуры на экране, то выполнение этой функции будет возможно только для объектов каждого из производных классов, определяющих конкретные изображения. В каждый из производных классов нужно включить свою функцию void show(), для формирования изображения на экране. Доступ к функции show() производного класса возможен с помощью явного указания ее полного имени, например:

triangle::show ();

или с использованием конкретного объекта:

triangle t;

t.show ();

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

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

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

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

struct base {

void fun (int i){

cout <<”base::i = ” << i << ’\n’;}

};

struct der: public base {

void fun (int i) {

cout << ” der::i = ” << i << ‘\n‘;}

};

void main () {

base B, *dp = &B;

der D, *dp = &D;

base pbd = &D; // Неявное преобразование от der* к base*.

bp->fun (1);

dp->fun (5);

pbd->fun (8);

}

Результат:

base::i = 1

der::i = 5

base::i = 8

Здесь указатель pbd имеет тип base*, однако его значение - адрес объекта D класса der. При вызове функции-члена по указателю на объект выбор функции зависит только от типа указателя, но не от его значения, что и иллюстрируется выводом base::i = 8. Настроив указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса. Таким способом не удается достичь позднего или динамического связывания.

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

class base {

public:

int i;

virtual void print (){

cout << i << “ внутри base\n“;}

};

class D: public base{

public:

void print (){

cout << i << “ внутри D\n“;}

};

void main (){

base b;

base *pb = &b;

D f;

f.i = 1 + (b.i = 1);

pb->print ();

pb = &f; // Неявное преобразование D* к Base*.

pb->print ();

}

Результат:

1 внутри base

2 внутри D

Здесь в каждом случае выполняется различная версия функции print(). Выбор динамически зависит от типа объекта,на который указывает указатель. Служебное слово virtual означает, что функция print() может иметь свои версии для различных порожденных классов. Указатель на базовый класс может указывать или на объект базового класса, или на объект порожденного класса. Выбранная функция-член зависит от класса, на объект которого указывается, но не от типа указателя. При отсутствии члена производного типа по умолчанию используется виртуальная функция базового класса. Отметим различие между выбором соответствующей переопределенной виртуальной функции и выбором перегруженной функции-члена (не виртуальной): перегруженная функция-член выбирается во время компиляции алгоритмом, основанным на правиле сигнатур. При перегрузке функции-члены могут иметь разные типы возвращаемого значения. Если же функция объявлена как virtual, то все её переопределения в порожденных классах должны иметь одну и ту же сигнатуру и один и тот же тип возвращаемого значения. При этом в производных классах слово virtual можно и не указывать.

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

Рассмотрим пример.

Вычисление площадей различных фигур.

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

class figure {

protected:

double x, y;

virtual double area (){return 0;} // Площадь по умолчанию.

};

class rectangle: public figure {

private:

double height, width;

. . .

public:

rectangle (double h, double w){height=h; width=w;}

double area () {return height * width;}

. . .

};

class circle: public figure {

private:

double radius;

. . .

public:

circle (double r){radius=r;}

double area () {

return M_PI*radius*radius;}

. . .

};

Код пользователя может выглядеть так:

const N = 30;

figure *p[N];

double tot_area = 0;

. . . // Здесь устанавливаются указатели p[i], например,

. . . // rectangle r(3, 5); p[0]=&r; circle c(8); p[1]=&c; и т.д.

for (i = 0; i < N; i ++) tot_area + = p[i]->area (); // Код пользователя.

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

Рассмотрим еще пример для расчета заработной платы с классом Employee.

#include <iostream.h>

#include <fstream.h>

#include <conio.h>

#include <string.h>

class Employee{

protected:

char * firstName, * lastName; // Имя, фамилия.

int age; // Возраст.

double payRate; // Размер оплаты.

public:

Employee (char* FN, char* LN, int a, double pay){

firstName = new char [strlen (FN) + 1];

strcpy(firstName, FN);

lastName = new char [strlen (LN) + 1];

strcpy(lastName, LN);

age = a;

payRate = pay;

}

virtual double CalculatePay (){

return 0;

}

void print(){cout<<firstName<<" "<<lastName<<" this month has received ";}

virtual ~Employee (){};

};

class HourlyEmployee: public Employee { // Почасовая оплата.

int hours; // Проработано часов.

public:

HourlyEmployee (char* FN, char* LN, int a, double pay, int h):

Employee (FN, LN, a, pay){ hours=h; }

virtual ~HourlyEmployee (){delete firstName; delete firstName;}

virtual double CalculatePay (){

return hours*payRate;

}

};

class ContractorEmployee: public Employee { // Работа по контракту. public:

ContractorEmployee (char* FN, char* LN, int a, double pay):

Employee (FN, LN, a, pay){}

virtual double CalculatePay (){

return payRate;

}

virtual ~ContractorEmployee (){delete firstName; delete firstName;}

};

class DaypaymentEmployee: public Employee { // Поденная оплата.

int days; // Отработано дней.

public:

DaypaymentEmployee (char* FN, char* LN, int a, double pay, int d):

Employee (FN, LN, a, pay){days=d;}

virtual double CalculatePay (){

return days*payRate/24.0; // Рабочих дней в месяце – 24.

}

virtual ~DaypaymentEmployee (){delete firstName; delete firstName;}

};

void loademploee (Employee* a[], int &n){

char FN[20], LN[20]; int age, arg; double pay;

char sel; // Селектор, задающий тип оплаты.

ifstream file ( "emp.dat" ); // Создаем входной поток для чтения

// file и связываем его с внешним

// файлом emp.dat.

n = 0;

while ( file.peek ( ) != EOF ){ // Пока нет конца файла …

file >> sel;

file >> FN;

file >> LN;

file >> age;

file >> pay;

file >> arg;

switch (sel){

case 'h': a[n] = new HourlyEmployee (FN, LN, age, pay, arg);

break;

case 'c': a[n] = new ContractorEmployee (FN, LN, age, pay);

break;

case 'd': a[n] = new DaypaymentEmployee (FN, LN, age, pay, arg);

break;

}

n++;

}

}

void main(){

int n;

Employee* a[20];

clrscr();

loademploee (a, n);

double s=0, r;

for(int i=0; i<n; i++){

s+=(r=a[i]->CalculatePay ());

a[i]->print();

cout.width (16); cout << r << "$\n";

}

cout<<"This month it is payd: ";

cout.width (16); cout << s << "$\n";

}

Если во входном файле emp.dat будет содержаться информация

c Gbanov Ivan 32 4340 0

c Muhin Sergey 26 1320 0

h Mazin Petr 27 15.3 32

d Bobrov Mikhail 40 110 21

то в результате работы программы на экране появится следующее:

Gbanov Ivan this month has received 4340$

Muhin Sergey this month has received 1320$

Mazin Petr this month has received 489.6$

Bobrov Mikhail this month has received 96.25$

This month it is payd: 6245.85$

 

 



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

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