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



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

9.6. Битовые поля
Описатель-компонента вида
идентификатор opt : константное выражение
определяет битовое поле длиною, определяемому константным выражением.
Битовые поля пакуются в некоторую адресуемую единицу памяти. Неименованное битовое поле полезно для разметки в соответствии с установленными извне схемами. Оно не считается компонентом и не может быть инициализировано.
Битовое поле должно иметь целочисленный тип. Операция взятия адреса неприменима к нему, поэтому указателей на битовые поля нет.
9.7. Вложенные объявления классов
Класс, объявленный внутри другого класса, является вложенным (nested). Имя вложенного класса является локальным в его объемлющем классе. Не считая обычной работы через явные указатели, ссылки и имена объектов, объявления во вложенном классе могут использовать из объемлющего класса только имена типов, статических компонентов и перечислителей. Пример:
int x;

int y;
class enclose {

public:

int x;

static int s;
class inter {

void f(int i)

{

x = i; // ошибка: присваивание enclose::x

s = 1; // верно: присваивание enclose::s

::x = 1; // верно: присваивание глобальной x

y = i; // верно: присваивание глобальной y

};

void g(enclose* p, int i)

{

p->x = i; // верно: присваивание enclose::x

};

};

};
inner* = 0; // ошибка: "inner' вне области действия
Компонентные функции как объемлющего класса, так и вложенного класса подчиняются обычным правилам доступа. Пример:
class E {

int x;

class I {

int y;

void f(E* p, int i)

{

p->x = i; // ошибка: E::x скрытый компонент класса

};

};

int g(I* p)

{

return p->y; // ошибка: I::y скрытый компонент класса

};

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

struct M1 {int m};

public:

struct M2 {int m};

M1 f(M2);

};
void f()

{

M1 a; // ошибка: имя "М1" невидимо

M2 b; // ошибка: имя "М2" невидимо

X::M1 c; // ошибка: "Х::М1" скрытый компонент класса

X::M2 d; // все в порядке

}
9.8. Локальные объявления классов
Класс, описанный внутри функции, называется локальным. Объявления в локальном классе могут использовать из объемлющей области действия только имена типов, статические переменные, внешние переменные и элементы перечисления (разрешение использования автоматических переменных объемлющей функции предполагало бы допустимость вложенных функций). Пример:
int x;

void f()

{

static int s;

int x;

extern int g();

struct local {

int g() {return x;} // ошибка: "x" автоматическая переменная

int h() {return s;} // верно

int k() {return ::x;} // верно

int l() {return g();} // верно

};

};

local* p = 0; // ошибка: "local" вне области действия
Компонентные функции локального класса должны быть описаны в нем. Локальный класс не может иметь статических компонентных данных. Цель этих ограничений - избежание сложных локальных классов.
9.9. Локальные имена типов
Имена типов подчиняются тем же правилам области действия, что и другие имена. Тем самым, имена типов, определенных внутри объявления класса, нельзя употреблять без уточнения "::" вне этого класса. Пример:
class X {

public:

typedef int I;

class Y { ... };

I a;

};

I b; // ошибка

Y c; // ошибка

X::Y d; // нормально
Имя-класса, описанное-имя-типа или имя константы, использованное в имени типа, не может переопределяться в объявлении класса после его использования в нем. Пример:
typedef int c;

enum { i =1};



class X {

char v[i];

int f() {return sizeof(c)};

char c; //ошибка: имя типа переопределяется после использования

enum {i =2}; // ошибка: константа "i" переопределяется после исп-ния

};
typedef char* T;
struct Y {

T a;

typedef long T; // ошибка: Т уже использован

T b;

};

10. Производные классы
Для того чтобы отразить иерархические связи, существующие между понятиями и выражающие общность между классами, вводится понятие производного класса и связанные с ним понития языка. Например, понятие треугольника и окружности связаны друг с другом, потому что оба являются фигурами. Для того чтобы ввести треугольники и окружности в программу, не упустив при этом поняте фигуры, следует явно определить классы circle и triangle как включенные в общий класс shape. Данная глава содержит выводы из этой простой идеи, которая лежит в основе того, что называется объектно-ориентированным программированием.
При определении производного класса список его базовых классов задается при помощи следующей нотации:
спецификация-базы:

: список-баз


список-баз:

описатель-базы

список-баз, описатель-базы
описатель-базы:

полное-имя-класса



virtual спецификатор доступа opt полное-имя-класса

спецификатор доступа virtual opt полное-имя-класса


спецификатор доступа:

private

protected

public
Полное-имя-класса в описателе-базы должно обозначать ранее объявленный класс, который называется в таком случае базовым классом. Если компоненты базового класса не переопределены в производном классе, они обозначаются и трактуются так же как и компоненты производного класса. В таком случае говорят, что компоненты базового класса наследуются производным классом. Производный класс может сам, в свою очередь, служить базовым классом с соответствующим контролем доступа.
В качестве примера рассмотрим построение программы, которая имеет дело с людьми, служащими в некоторой фирме. Структура данных в этой программе может быть следующей:
struct employee {

char* name;

short age, department;

int salary;

employee* next; // список однотипных служащих

// . . .

};
Определим тип менеджера:
struct manager {

employee emp; // сведения о менеджере как о служащем

employee* group // подчиненные люди

short level; // уровень

// . . .

};
Менеджер является также служащим; относящиеся к служащему данные хранятся в компоненте emp класса manager. Здесь нет, однако, ничего, выделяющего компонент emp для компилятора. Указатель на менеджера (manager*) не является указателем на служащего (employee*), поэтому использовать один просто там, где требуется другой, нельзя. В частности, нельзя поместить менеджера в список служащих, не написав для этого специальный фрагмент программы. Корректный подход заключается в том, чтобы установить, что менеджер является служащим с некоторой дополнительной информацией:
struct manager: employee {

employee* group // подчиненные люди

short level; // уровень

// . . .

};
Здесь manager является производным для employee и, обратно, employee есть базовый класс для manager. Указатель на объект производного класса может быть преобразован в указатель на объект однозначно доступного базового класса (4.6). Точно также ссылка на объект производного класса может быть преобразован в ссылку на объект однозначно доступного базового класса (4.7). Имея объявления employee и manager, мы можем теперь создать список служащих, некоторые из которых являются менеджерами:
void f()

{

manager m1, m2;

employee e1, e2;

employee* elist;

elist = &m1; // поместить m1 в elist

m1.next = &e1; // поместить e1 в elist

e1.next = &m2; // поместить m2 в elist

m2.next = &e2; // поместить e2 в elist

e2.next = 0; // завершить elist

};
Здесь было использовано неявное преобразования указателя на объект производного класса в указатель на объект базового класса. Обратное преобразование должно быть явным, чтобы удостовериться, что указатель действительно направлен на объект данного производного класса; например:
void g()

{

manager mm;

employee* pe = &mm; // OK, каждый менеджер является служащим

employee ee;

manager* pm = ⅇ // ошибка: не каждый служащий является менеджером

pm = (manager*) pe; // OK, поскольку 'ре' на самом деле ссылается на 'mm'

pm->level = 2; // OK, у менеджера есть место для 'level'

};
Заметим, что в типичной реализации С++ отсутствуют проверки на стадии выполнения программы, которые гарантируют, что указатель на объект базового класса действительно ссылается на объект производного класса (см. варианты решения проблемы в 10.1.1 и 10.2)
Просто структуры данных вроде employee и manager не особенно полезны, поэтому рассмотрим, как добавить в них функции. Пример:
class employee {

char* name;

// . . .

public:

employee* next;

void print() const;

// . . .

};
class manager: public employee {

// . . .

public:

void print() const;

// . . .

};
Здесь возникают несколько вопросов:
1) Как может компонентная функция производного класса использовать компоненты базового класса?

2) Как компоненты базового класса могут использовать компонентные функции производного класса?


Рассмотрим:
void manger::print() const

{

cout << " имя " << name << '\n';

};
Компонентная функция производного класса может использовать открытое имя из своего базового класса так же как это могут делать компоненты базового класса, т.е. без указания объекта (this предполагается неявно). Однако функция manger::print компилироваться не будет, так как компонентная функция производного класса не имеет особого права доступа к скрытым компонентам своего базового класса. Обычно самое простое решение - использовать открытые компоненты своего базового класса, например:
void manger::print() const

{

employee::print(); // печатает информацию о служащем

// печатает информацию о менеджере

};
Класс называется непосредственным базовым классом, если он упоминается в списке-баз, и косвенным базовым классом, если он является базовым классом для одного из классов, упомянутых в списке-баз. Имя в нотации имя-класса::имя может быть именем компонента косвенного базового класса; эта запись просто определяет класс, в котором начинается поиск имени. Пример:
class A {public: void f();};

class B : public A {};

class C : public B {public: void f();};
void C::f()

{

f(); //вызов f() из С

A::f(); //вызов f() из A

B::f() //вызов f() из A

};
Здесь A::f() вызывается оба раза, поскольку она - единственная f() в В.
10.1 Множественные базовые классы
Класс может быть порожден из любого числа базовых классов. Пример:
class A {/* . . . */};

class B {/* . . . */};

class C {/* . . . */};

class D: public A, public B, public C {/* . . . */};
Наличие нескольких непосредственных базовых классов называется множественным наследованием (multiple inheritance).
Класс не может быть задан в качестве непосредственного базового класса более одного раза, но он может быть более одного раза косвенным базовым классом. Пример:
class B {/* . . . */};

class D: public B, public B {/* . . . */}; // неверно
class L {/* . . . */};

class A: public L {/* . . . */};

class E: public L {/* . . . */};

class C: public A, public Е {/* . . . */}; // верно
В последнем случае объект класса С будет иметь два подобъекта класса L. Класс не может появляться дважды в списке базовых классов по той простой причине, что каждая ссылка на него была бы неоднозначна.
10.1.1. Неоднозначности
Два базовых класса могут иметь компонентные функции с одним и тем же именем, например:
class task {

// . . .

virtual debug_info* get_debug();

};
class displayed {

// . . .

virtual debug_info* get_debug();

};
При использовании обоих классов для порождения класса satellite:
class satellite : public task, public displayed

{

// . . .

};
неоднозначность в этих функциях должна быть устранена:
void f(satellite* sp)

{

debug_info* dip = sp->get_debug(); // ошибка: неоднозначность

dip1 = sp->task::get_debug(); // OK

dip2 = sp->displayed::get_debug(); // OK
Это решение не очень удачно, лучше описать новую функцию в производном классе:
class satellite : public task, public displayed

{

// . . .

debug_info* get_debug()

{

debug_info* dip1 = task::get_debug();

debug_info* dip2 = displayed::get_debug();

return cond? dip1 : dip2;

};

};
Таким образом информация о базовых классах локализована, всякий раз при вызове get_debug() для объекта класса satellite будет гарантированно вызываться satellite::get_debug().
10.1.2. Поле типа
Чтобы использовать производные классы не только как удобную сокращенную запись в описаниях, надо разрешить следующую простую проблему: если задан указатель вида base*, какому производному типу в действительности принадлежит указываемый объект? Есть три основных способа решения этой проблемы:
1) Обеспечить, чтобы всегда указывались только объекты одного типа.

2) Поместить в базовый класс поле типа, просматриваемое функциями.

3) Использовать виртуальные функции (10.2).
Обычно указатели на базовые классы используются при разработке контейнерных классов: множество, вектор, список и т.п. В этом случае решение 1 дает однородные списки. Решения 2 и 3 можно использовать для построения неоднородных списков, т.е. списков указателей на объекты разных типов.
Исследуем снячала решение 2, исследующее поля типа. Пример со служащими и менеджерами можно переопределить так:
struct employee {

enum employee_type {m, e};

employee_type type;

char* name;

short department;

employee* next; // список однотипных служащих

// . . .

};
struct manager : employee {

employee* group // подчиненные люди

short level; // уровень

// . . .

};
Теперь мы можем написать функцию, печатающую информацию о каждом служащем:
void print_employee (const employee* e)

{

switch (e->type) {

case e:

cout << e->name << '\t' << e->department << '\n'

// . . .

break;

case m:

cout << e->name << '\t' << e->department << '\n';

// . . .

manager p = (manager*)e;

cout "уровень" << p->level << '\n';

// . . .

break;

};

};
и воспользоваться ею, чтобы напечатать список служащих:
void f(const employee* elist)

{

for (; elist; elist = elist->next) print_employee(elist);

}
Такой вариант хорошо работает в одной программе, написанной одним человеком, но имеет большой недостаток необходимости явной проверки типа. В больших программах это обычно приводит к ошибкам двух сортов:
1) невыполнение проверки поля типа

2) учет не всех случаев в переключателе.


10.2. Виртуальные функции
Если класс base содержит виртуальную (virtual, 7.1.2) функцию vf, а класс derived, порожденный из класса base, также содержит функцию vf того же типа, то обращение к vf для объекта класса derived вызывает derived::vf (даже при доступе через указатель или ссылку на base). В таком случае говорят, что функция производного класса подменяет (override) функцию базового класса. Если, однако, типы этих функций различны, то функции считаются различными и механизм виртуальности не включается.
Виртуальные функции преодолевают те сложности, которое несет решение с использованием полей типа, позволяя программисту описывать в базовом классе функции, которые можно переопределять в производном классе. Пример:
class employee {

char* name;

short department;

// . . .

employee* next;

static employee* list;

public:

employee(char* n, int d);

virtual void print() const;

};
Здесь ключевое слово virtual указывает, что могут быть разные варианты функции print() для разных производных классов, и что поиск подходящей из них является задачей компилятора. Виртуальная функция базового класса должна быть описана либо объявлена "чистой" (pure, 10.3). Пример:
void employee::print() const

{

cout << this->name << '\t' << this->department << '\n';

}
Виртуальная функция может, таким образом использоваться даже в том случае, когда нет производных классов от ее класса. В производном классе, в котором не нужен специальный ее вариант, ее задавать нет необходимости. Но если нужен специальный вариант, он задается, например:
class manager : public employee {

employee* group // подчиненные люди

short level; // уровень

// . . .

public:

manager(char*, int l, int d);

// . .

void print() const;

};
void manager::print() const

{

cout << this->name << '\t' << this->department << '\n';

cout "уровень" << this->level << '\n';

}
Функция print_employee() теперь не нужна, поскольку ее место заняли функции print(). Список служащих теперь может быть распечатан следующим образом:
void employee::print_list()

{

for (employee* p = list; p; p = p->next) p->print();

}
Каждый служащий будет теперь печататься в соответствии со своим типом. Например,
int main()

{

employee e("J. Brown", 1234);

manager m("J. Smith", 2, 1234);

employee::print_list();

}
выдаст
J. Brown 1234

уровень 2

J. Smith 1234
Явное уточнение операцией области действия подавляет механизм виртуальных функций. Пример:
class B {public: virtual void f();};

class D public B {public: virtual void f();};
void D::f() { /* . . . */ B::f();}
Здесь при исполнении f из D реально произойдет вызов B::f(), а не D::f(), чтобы избежать рекурсии.
10.2.1. Виртуальные базовые классы
Иногда возникает необходимость в тесной связи "братских" (т.е. потомков одного прародителя) базовых классов для распределении информации между ними. Для этого служит механизм виртуальных (virtual) базовых классов. Добавление к описателю базового класса ключевого слова virtual приводит к тому, что единственный объект виртуального базового класса используется каждым его производным классом. Рассмотрим пример построения окон. Определим сначала базовый класс:
class window {

// начальное наполнение

virtual void draw();

};
Для простоты мы рассматриваем только один компонент класса window - функцию draw(). Другие окна могут быть построены как производные классы от window. Каждый из них определяет свою собственную (детально разработанную) версию этой функции, например:
class window_w_border :

public virtual window{ // окно с границами

// наполнение границы

void draw();

};
class window_w_menu :

public virtual window{ // окно с меню

// наполнение меню

void draw();

};
Теперь мы можем определить окно с границами и меню:
class window_w_border_and_menu :

public window_w_border,

public window_w_menu {

void draw();

};
Каждый такой производный класс дополняет окно некоторыми новыми свойствами. Чтобы использовать их в сочетании друг с другом, мы должны гарантировать, что в обоих случаях вхождения класса window в его производные классы используется один и тот же объект класса window. Именно этого мы и достигли, когда специфицировали window как виртуальный базовый класс для производных окон: объект класса window_w_border_and_menu содержит только один объект класса window
Далее мы должны запрограммировать различные функции draw(). Попробуем сделать это самым простым способом, что неизбежно приведет к проблемам:
void window_w_border::draw()

{

window::draw();

// нарисовать границы

};
void window_w_menu::draw()

{

window::draw();

// нарисовать меню

};
До сих пор все нормально, поскольку все идет по образцу простого наследования. Однако на следующем уровне обнаруживаются проблемы:
void window_w_border_and_menu::draw()

{

window_w_border::draw();

window_w_menu::draw();

// действия, характерные для window_w_border_and_menu

};
Данная функция будет вызывать window::draw() дважды, что может вызвать искажения на экране. Чтобы избежать этого надо вернуться на шаг назад и отделить работу, выполняемую базовым классом, от работы, выполняемой производными классами. Для этого снабдим каждый класс двумя функциями:
1) функцией _draw(), делающей только то, что требует данный класс и

2) функцией draw(), делающей и то, что требуется в ее классе, и то, что требуется в базовых классах.


class window {

// начальное наполнение

void _draw();

virtual void draw() { _draw();};

};
class window_w_border :

public virtual window{ // окно с границами

// наполнение границы

void _draw();

void draw() { window::_draw(); _draw(); };

};
class window_w_menu :

public virtual window{ // окно с меню

// наполнение меню

void _draw();

void draw() { window::_draw(); _draw(); };

};
Различие с предыдущем вариантом становится очевидным на следующем этапе:
class window_w_border_and_menu :

public window_w_border,

public window_w_menu {

void _draw();

void draw();

};
void window_w_border_and_menu::draw()

{

window::_draw()

; window_w_border::_draw();

window_w_menu::_draw();

_draw(); // действия, характерные для window_w_border_and_menu

};
Теперь функция window::_draw() вызывается только один раз. Заметим, что класс window используется как хранилище информации, используемой и window_w_border, и window_w_menu.


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


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

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