С++ обзор языка



страница8/10
Дата22.06.2019
Размер1.54 Mb.
1   2   3   4   5   6   7   8   9   10

10.3. Абстрактные классы
Механизм абстрактных классов служит для представления общих понятий, как, например, фигура, которые фактически используются лишь для порождения более конкретных понятий; например из фигуры порождается окружность или квадрат. Абстрактный класс можно также употреблять как определение интерфейса, для которого производные классы обеспечивают разнообразие реализаций.
Абстрактный класс - это класс, который может использоваться только лишь в качестве базового для некоторого другого класса; никакие объекты абстрактного класса не могут создаваться иначе как подобъекты объектов его порожденного класса. Это видно хотя бы из того, что для виртуальных функций абстрактного класса нельзя дать разумного определения. Пример:
class shape {

// . . .

public:

virtual void rotate(int) {error(shape::rotate); };

virtual void draw() {error(shape::draw); };

};
Наивная попытка создать неопределенную фигуру:
shape s; // глупо: бесформенная фигура
бессмысленна.
Класс является абстрактным, если он содержит хотя бы одну чисто виртуальную функцию (pure virtual function), т.е. функцию, в объявлении которой задан чистый спецификатор "=0" (9.2). Пример:
class shape {

// . . .

public:

virtual void rotate(int) = 0; // чисто виртуальная функция

virtual void draw()= 0; // чисто виртуальная функция

};
Попытка создания объекта такого класса, например:
shape s; // ошибка: переменная абстрактного класса
ведет к ошибке. Пример использования абстрактного класса:
class circle: public shape {

int radius;

public:

void rotate(int) {}; // все в порядке: замещает shape::rotate

void draw(); // все в порядке: замещает shape::draw

circle(point p, int r);

};
Чисто виртуальная функция, не определенная в производном классе, остается чисто виртуальной функцией, так что производный класс будет абстрактным. Это позволяет строить реализацию по шагам:
class X {

public:

virtual void f() = 0;

virtual void g() = 0;

};
X b; // ошибка: объявление объекта абстрактного класса Х
class Y: public X {

void f(); // замещает X::f

};
Y b; // ошибка: объявление объекта абстрактного класса Y
class Z: public Y {

void g(); // замещает X::g

};
Z c; // OK

11. Управление доступом к компонентам классов
Компонент класса может быть
1) скрытым (private); это означает, что его имя может употребляться только внутри его класса и друзьями его класса;

2) защищенным (protected); это означает, что его имя может употребляться только внутри его класса, друзьями его класса и в производных классах;

3) открытым (public); это означает, что его имя может употребляться везде.
При таком подходе компоненты класса (функции, данные и т.д.) по их возможностям использования делятся на три категории:
1) компоненты, реализующие интерфейс самого класса;

2) компоненты, реализующие интерфейс производного класса, и



3) другие компоненты.
Компоненты класса, объявленного с ключевым словом class, являются по умолчанию скрытыми. Компоненты класса, объявленного с ключевым словом struct или union, являются по умолчанию открытыми. Пример:
class X {

int a; // "а" скрытый компонент класса

};
struct S {

int a; // "а" открытый компонент класса

};
11.1 Спецификаторы доступа
Спецификатор-доступа определяет права доступа к компонентам класса, следующим за ним до конца объявления класса или до следующего спецификатора-доступа, например:
class X {

int a; // "а" скрытый компонент класса по умолчанию

public:

int b; // X::b - открытый

int c;; // X::с - открытый

};
Допускается любое число спецификаторов доступа в произвольном порядке:
struct S {

int a; // "а" открытый компонент по умолчанию

protected: int b;

private: int c;

public int d;

};
11.2. Спецификаторы доступа для базовых классов
Если класс объявляется базовым для некоторого другого класса посредством спецификатора доступа public, то открытые компоненты базового класса становятся открытыми компонентами и производного класса, а защищенные - защищенными. Если же класс объявляется базовым для некоторого другого класса посредством спецификатора доступа protected, то открытые и защищенные компоненты базового класса становятся защищенными компонентами производного класса. Скрытые же компоненты базового класса становятся недоступными для производного класса, если только они не объявлены в базовом классе как friend. Если же класс объявляется базовым для некоторого другого класса посредством спецификатора доступа private, то открытые и защищенные компоненты базового класса становятся скрытыми компонентами производного класса. Скрытые же компоненты базового класса становятся недоступными для производного класса, если только они не объявлены в базовом классе как friend. При отсутствии спецификатора-доступа при базовом классе подразумевается public, если производный класс объявлен со struct, и подразумевается private, если производный класс объявлен с class. Примеры:
class B { /* . . . */};

class D1 : private B { /* . . . */};

class D2 : public B { /* . . . */};

class D3 : B { /* . . . */}; // "В" скрыт по умолчанию

struct D4 : B { /* . . . */}; // "В" открыт по умолчанию
Рассмотрим теперь следующие классы:
class X { public int a; // . . .

};
class Y1: public X {};

class Y2: protected X {};

class Y3: private X {};
Так как Х - это открытый базовый класс для Y1, то любая функция может (неявно) преобразовывать указатель типа Y1* к Х* в точности так же, как она может получить доступ к открытым компонентам класса Х.
Так как Х - это защищенный базовый класс для Y2, то только компоненты и друзья класса Y2, а также компоненты и друзья классов, производных от Y2, могут (неявно) преобразовывать указатель типа Y2* к Х* в точности так же, как они могут получить доступ к открытым и защищенным компонентам класса Х.
Так как Х - это скрытый базовый класс для Y3, то только компоненты и друзья класса Y3 могут (неявно) преобразовывать указатель типа Y2* к Х* в точности так же, как они могут получить доступ к открытым и защищенным компонентам класса Х.
Объявление базового класса скрытым не влияет на доступ к статическим компонентам базового класса.
11.3. Объявления доступа
Доступ к компоненту базового класса в производном классе можно скорректировать, упомянув его уточненное-имя в открытой или защищенной части объявления производного класса. Это называется объявлением доступа (access declaration). Пример:
class B {

int a;

public:

int b, c;

int bf();

};
class D : private B {

int d;

public:

B::c; // доступ к 'B::c'

int e;

int df();

};
int ef(D&);
Здесь функция ef может использовать лишь имена с, е и df. Являясь компонентом класса D, функция df может использовать имена b, c, bf, d, e и df, но не а. Являясь компонентом класса, функция bf может использовать имена а, b, c и bf.
Объявление доступа не может изменять права доступа к компоненту базового класса по отношению к объявленным в нем. Пример:
class B {

public:

int a;

private:

int b;

protected:

int c;

};
class D : private B {

public:

B::a; // делает "а" открытым компонентом класса D

B::b; // ошибка: попытка усилить доступ

protected:

B::c; // делает "с" защищенным компонентом класса D

B::a; // ошибка: попытка ослабить доступ

};
Доступ к компоненту базового класса не может быть скорректирован в производном классе если в нем объявляется компонент с тем же именем. Пример:
class X {

public void f();

};
class Y : private X {

public:

void f(int);

X::f; // ошибка: два объявления f

};
11.4. Друзья класса
Друг (friend) класса - это функция, которая не является компонентом класса, но которой разрешается использовать его защищенные и скрытые компоненты. Друга класса нельзя вызвать посредством операции доступа к компоненту класса, кроме случая, когда друг является компонентом другого класса. Пример:
class X {

int a;

friend void friend_set(X*, int);

public void member_set(int);

};
void friend_set(X* p, int i) {p->a = 1;};

void X::member_set(int i) {a = i};
void f

{

X obj;

friend_set(&obj, 10);

obj.member_set(10);

};
Механизм друзей важен потому, что функция может быть другом двух классов и потому - гораздо эффективнее обычной функции, оперирующей объектами обоих классов. Пример:
class matrix;
class vector {

float v[4];

// . . .

friend vector multiply(const matrix&, const vector&);

};
class matrics {

vector v[4];

// . . .

friend vector multiply(const matrix&, const vector&);

};
Мы можем теперь написать функцию умножения, которая использует элементы векторов и матриц непосредственно:
vector multiply(const matrix& m, const vector& v)

{

vector r;

for (int i = 0; i < 3; i++) { // r[i] = m[i] * v

r.v[i] = 0;

for (int j = 0; j < 3; j++) r.v[i] += m.v[i][j] * v.v[j];

};

return r;

};
Компонентная функция одного класса может быть другом другого:
class X {

// . . .

void f();

};
class Y {

// . . .

friend void X::f();

};
Сразу все компонентные функции класса Х могут быть объявлены друзьями класса Y одним объявлением с уточненным-спецификатором-типа (9.1):
class Y {

// . . .

friend class X;

};
Функция, первое объявление которой содержит спецификатор friend, считается также внешней (3.3, 7.1.1). Из сказанного следует:
static void f() { /* . . . */};
class X { friend g(); }; // подразумевает и extern g()
class Y {

friend void f(); // верно: f() имеет теперь внутренне связывание

};
static g() { /* . . . */}; // ошибка: несовместимое связывание
11.5. Доступ к защищенным компонентам класса
Дружественная функция или компонентная функция производного класса может иметь доступ к защищенному статическому компоненту базового класса. Дружественная функция или компонентная функция производного класса D может иметь доступ к защищенному нестатическому компоненту базового класса только по указателю или ссылке на D либо через объект класса D (или любого производного от него). Пример:
class B {

protected int i;

};
class D1: public B {

};
class D2: public B {

friend void fr(B*, D1*, D2*);

void mem(B*, D1*);

};
void fr(B* pb, D1* p1, D2* p2)

{

pb->i = 1; // запрещено

p1->i = 2; // запрещено

p2->i = 3; // верно: доступ через D2

};
void D2::mem(B* pb, D1* p1)

{

pb->i = 1; // запрещено

p1->i = 2; // запрещено

i = 1; // верно: доступ через "this"

};
void g(B* pb, D1* p1, D2* p2)

{

pb->i = 1; // запрещено

p1->i = 2; // запрещено

p2->i = 3; // запрещено

};
11.6. Доступ к виртуальным функциям
Права доступа к виртуальной функции определяются ее объявлением и не заменяются на права доступа к функциям, которые позднее подменяют ее. Пример:
class B {

public virtual f();

};
class D : public B {

private f();

};
void f()

{

D d;

B* pb = &d;

D* pd = &d;

pb->f(); // верно: B::f() - открытая функция

pd->f(); // ошибка: D::f() - скрытая функция

};
11.7. Множественный доступ
Если некоторое имя может быть достигнуто в графе множественного наследования по нескольким путям, выбирается наибольший из возможных доступов. Пример:
class W {public: void f();};

class A: private virtual W {};

class B: public virtual W {};

class C: publicA, public B{

void f() {W::f();} // верно

};
Так как W::f() доступна из С::f() вдоль открытого пути через В, такой доступ возможен.

12. Специальные компонентные функции
Некоторые компонентные функции являются специальными в том смысле, что влияют на создание, копирование и уничтожение объектов класса или задают способ приведения значений к значениям других типов. Часто такие функции вызываются неявно.
Специальные компонентные функции подчиняются стандартным правилам доступа (11). Например, обявление конструктора объектов защищенным гарантирует, что создание объектов с его участием возможно только в производных классах и друзьях.
12.1. Конструкторы
Компонентная функция с тем же именем, что и у класса, называется конструктором; она используется для создания значений объектов этого класса. Пример:
class date {

// . . .

date(int, int, int);

};
Если у класса есть конструктор, каждый его объект будет инициализирован до какого бы ни было его использования. Это защищает программиста от работы с неинициализированными или многократно инициализированными объектами. Конструктор вызываеся неявно с возможными аргументами при создании объекта данного класса. Пример:
date xmas(25, 12, 0);

date today; // ошибка, пропущена инициализация


Можно задать несколько способов инициализации объектов, описав несколько конструкторов, например:
class date {

int month, day, year;

public:

// . . .

date(int, int, int); // день, месяц, год

date(int, int); // день и месяц текущего года

date(int); // день, месяц и год текущие

date(); // дата по умолчанию: сегодня

date(const char*); // дата в строковом представлении

};
Конструкторы подчиняются тем же правилам относительно типов параметров, что и функции с совмещенными именами, поэтому компилятор может выбрать нужный конструктор при каждом его использовании, например:
date today(4);

date july4("July 4, 1983");

date now; // инициализируется по умолчанию
Конструктор не может быть объявлен как константный или подвижный, не может быть виртуальным и статическим. Конструкторы не наследуются.
Конструктор копирования класса Х - это конструктор, который может быть вызван для копирования объекта класса Х, т.е. такой конструктор, у которого один из параметров имеет тип X&. Пример:
class X {

// . . .

public

X(int);

X(const X&, int = 1);

};
X a(1); // вызывается X(int)

X b(a, 0); // вызывается X(const X&, int)

X c = b; // вызывается X(const X&, int)
Конструктор не должен возвращать значение, поэтому оператор return в теле конструктора не может сопровождаться значением. Нельзя брать адрес конструктора.
Конструктор может быть вызван и явным образом в соответствии со следующим синтаксисом:
имя-класса (список-выражений opt)
Пример:
complex zz = complex(1, 2.3);

cprint(complex(7.8, 1.2));


12.3. Преобразования
Преобразования (изменения типа) объектов классов выполняются конструкторами и преобразующими функциями. Такие преобразования, называемые пользовательскими (user-defined), часто применяются в дополнение к стандартным преобразованиям (4).
12.3.1. Преобразование посредством конструктора
Конструктор с одним параметром задает преобразоание типа своего параметра к типу своего класса. Пример:
class X {

// . . .

public

X(int);

X(const char*, int = 0);

};
void f(X arg) {

X a = 1; // a =X(1)

X b = "Jessie"; // b = X("Jessie", 0)

a = 2; // a = X(2)

f(3); // f(X(3))

};
12.3.2. Преобразующие функции
Использование конструктора для задания преобразования типа удобно, но имеет нежелательные следствия:
1) неявное преобразование из пользовательского типа в базовый тип невозможно;

2) нельзя задать преобразование из нового типа в существующий;

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

Компонентная функция класса Х, имя которой имеет вид


имя-функции-приведения:

operator имя-приведенного-типа


имя-приведенного-типа:

список-спецификаторов-типа ptr-операция opt


определяет преобразование из Х в тип, заданный именем-приведенного-типа. Пример:
class X {

// . . .

public:

operator int();

};
void f(X a)

{

int i = int(a);

i = (int)a;

i = a;

};
Во всех трех случаях присваиваемое значение будет преобразовано посредством функции X::operator int(). Операции преобразования наследуются. Пример:
struct S {

operator int();

};
struct SS: public S {};
void f()

{

SS a;

int i = a; // i = a.S::operator int()

};
Пользовательские преобразования неявно применяются лишь в случае, когда они однозначны (неоднозначность может возникнуть как из-за выбора между пользовательскими преобразованиями, так и между полбзовательским и стандартным преобразованием).
12.4. Деструкторы
Деструктор служит для уничтожения объекта в памяти, когда исчезает необходимость в нем. Если Х - имя класса, тогда ~X - имя деструктора этого класса. Деструкторы полезны для освобождения памяти, выделяемой конструктором для объекта в свободной памати. Вот пример стека (без обработки ошибок):
class char_stack {

int size;

char* top;

char* s;

public:

char_stack(int sz) { top = s = new char[size = sz]; };

~char_stack() { delete[] s; } // деструктор

void push(char c) {*top++ = c; }

char pop() {return *--top;}

};
Когда объект типа char_stack исчезает из области видимости, вызывается деструктор:
void f()

{

char_stack s1(100);

char_stack s2(200);

s1.push('a');

s2.push(s1.pop());

char ch = s2.pop();

cout << ch << '\n';

};
При вызове функции f() вызывается конструктор char_stack, чтобы выделить массив из 100 символов для объекта s1 и массив из 200 символов для объекта s2. При возврате из f() оба этих массива будут освобождены.
Деструктор не может быть объявлен как константный или подвижный и не может быть статическим. Деструкторы не наследуются. Однако деструктор может быть виртуальным.
12.5. Конструкторы и деструкторы
Конструкторы и деструкторы применяются к объектам, создаваемым следующим образом.
1. Автоматический объект: создается каждый раз, когда его объявление встречается при выполнении программы, и уничтожается каждый раз при выходе из блока, в котором оно появилось.
2. Статический объект: создается один раз при запуске программы и уничтожается один раз при ее завершении.
3. Объект в свободной памяти: создается операцией new и уничтожается операцией delete.
4. Компонентный объект: компонент объекта другого класса или элемент массива.
В следующих подразделах предполагается, что объекты являются элементами класса, имеющего конструктор и деструктор, например:
class table {

name* tb1;

int size;

public:

table(int sz = 15);

~table();

name* look(char*, int = 0);

name* insert(char* s) { return look(s, 1) }

};
12.5.1. Локальные переменные
Деструкторы локальных переменных выполняются в порядке, обратном порядку их создания. Пример:
void f(int i)

{

table aa;

table bb:

if (i > 0) { table cc; /* . . . */ };

// . . .

};
В этом примере каждый раз при вызове функции f() создаются переменные: сначала аа, затем - bb, которые каждый раз уничтожаются по выходе из f() в таком порядке: сначала bb, затем - аа. Если i > 0, то, кроме того, последней создается и первой уничтожается переменная сс.


Поделитесь с Вашими друзьями:
1   2   3   4   5   6   7   8   9   10


База данных защищена авторским правом ©vossta.ru 2019
обратиться к администрации

    Главная страница