Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
278
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

670Часть IV * Расширенное использование C++

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

ипроектировщику клиентской программы и липу, осуществляющему сопровож­ дение.

Строгий контроль типов обеспечивает так называемое раннее связывание. Тип объектов вычислений фиксируется на этапе компиляции и не изменяется во время выполнения программы. Другой популярный термин — статическое связывание. Оно означает то же самое. Связь между именем объекта и типом объекта фикси­ руется при компиляции и не может изменяться динамически во время выполнения программы.

Когда сообщение, определенное его именем и списком фактических аргумен­ тов, посылается объекту, компилятор интерпретирует его в соответствии с клас­

сом (типом) этого объекта. Имя класса объекта известно на этапе компиляции и не изменяется во время выполнения.

Статическое связывание является стандартом таких современных языков, как С4-+, С, Java, Ada, Pascal (но не Lisp). Вначале оно было введено для повышения производительности, а не для улучшения качества программы. Динамическое свя­ зывание, поиск значения вызова функции во время выполнения отнимает время. Когда значение вызова функции фиксируется при компиляции, программа выпол­ няется быстрее.

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

Строгий контроль типов предусматривает контроль типов на этапе компиляции и повышает производительность во время выполнения. Это полезно в большин­ стве приложений.

Когда еще вам хотелось бы установить связь между идентификатором и объек­ том вычислений? Можно ответить: на этапе выполнения.

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

Например, программа может выводить на экран изображения, показывая со­ ставляющие их фигуры одну за другой. Программа должна вызвать Circle:: clraw(), Square: :clraw(). Rectangle: :draw() или еще какую-либо известную ей фигуру. Это было бы замечательно. Однако лучше всего использовать только один оператор в исходной программе и изменять его значение в зависимости от фактической природы объекта shape.

shape. drawO; / / из класса Circle, Square или Rectangle

Если формой объекта в текущем проходе цикла является Circle, то этот опера­ тор вызывает Circle: :cJraw(). Если — Square, то следует задать Square: :draw(). Если это Rectangle, вызовите Rectangle: :clraw().

При строгом контроле за типами это невозможно. Компилятор найдет объяв­ ление переменной shape, установит ее класс и проверит определение класса. Если в этом классе не будет найдена пустая функция draw() с отсутствующими парамет­ рами, компилятор генерирует сообщение об ошибке. Если функция обнаружена, компилятор генерирует объектный код. Но тип функции draw() во время компи­ ляции будет фиксированным. Во время выполнения уже будет невозможно осуще­ ствлять поиск значения функции draw().

FACULTY
U12345678
Smith, John Associate Professor 1
STUDENT
U12345611
Jones. Jan Computer Science FACULTY U12345689
Black, Jeanne Assistant Professor STUDENT
U12345622 Green, James Astronomy
Рис. 15.9.
Входные данные для примера динамического связывания

Глава 15 • Виртуальные функции и использование наследования

671

То, что нам в данный момент нужно, называется связыванием этапа выполне­ ния или поздним динамическим связыванием. Предположим, что существует не­ сколько объектов вычисления (функций drawO в различных классах). Требуется связать один из этих объектов вычислений с именем в вызове конкретной функ­ ции. Более того, желательно, чтобы эта функция draw() в представленном вызове функции означала Circle: :draw(), Square: :draw(), Rectangle: :draw() и т.д. Хотелось бы, чтобы это значение устанавливалось не на этапе компиляции, а при выполнении. Тогда различные фигуры будут нарисованы в зависимости от значе­ ния вызова функции.

Поговорим о терминологии. Техническим термином для установки значения имени функции является связывание (binding). Компилятор связывает имя функ­ ции с конкретной функцией. Желательно, чтобы это связывание имело место на этапе выполнения. Поэтому оно называется динамическим связыванием, а не свя­ зыванием при компиляции. Нужно, чтобы связывание происходило после этапа компиляции, поэтому оно называется поздним, а не ранним, связыванием. Требу­ ется, чтобы такое связывание допускало присваивание различных значений тому же самому имени функции в зависимости от природы используемого объекта. Именно поэтому оно называется динамическим, а не статическим связыванием.

Способность имени функции принимать при своем вызове различные значения называется полиморфизмом (от "множество форм"). Некоторые авторы исполь­ зуют термин "полиморфизм" в более широком смысле, включая использование одного имени функции в различных классах, но без динамического связывания. Обратите внимание, что при использовании полиморфизма подразумевается позднее или динамическое связывание, присвоение значения вызова метода на этапе выполнения в зависимости от фактического типа объекта, т. е. назначение сообш,ения.

Все это должны обеспечить виртуальные функции C+ + .

Динамическое связывание: традиционный подход

Динамическое связывание не является каким-то особенным вопросом для объектно-ориентированного программирования. Обработка неоднородных списков всегда была обычной вычислительной задачей, и программисты привыкли реализовывать динамическое связывание на любом языке. Наша задача заключалась в обработке подобных объектов. Они настолько подобны, что имеет смысл исполь­ зовать одно имя для функции во всех категориях объектов (например, clraw()). Но типы объектов не являются идентичными — каждая функция выполняет заданные действия по-своему.

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

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

672

struct Person {

/ /

1 для преподавателей, 2 для студента

int kind;

char

id[10];

/ /

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

char*

name;

/ /

переменной длины

char*

rank;

/ /

только для преподавателей

char*

major; }

/ /

только для студента

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

В первом традиционном подходе характеристики различных видов объектов объединяются (например, служебное положение, специализация) в один класс. Чтобы обработать каждый вид объектов по-разному, добавляется поле для описа­ ния, к какой области принадлежит конкретный объект. В клиентской программе используются либо операторы выбора switch, либо операторы if, ветви которых реализуют обработку различных видов объектов.

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

Динамическое управление памятью сохраняет пространство и предотвращает переполнение памяти. Простым методом сохранения данных в памяти является определение массива объектов Person. Хотя и используется зарезервированное слово struct, переменные типа Person являются объектами, потому что в С+ + зарезервированные слова struct и class — синонимы (за исключением прав до­ ступа по умолчанию и наследования по умолчанию).

Person data [1000]; / / массив входных данных

Рекомендуем вам хранить данные в массиве указателей на объекты, а не в мас­ сиве объектов. Для выделения большого массива указателей не требуются боль­ шие затраты памяти. В случае переполнения массив указателей можно повторно выделить, не копируя суш,ествуюил,ие данные (см. главу 6). Пространство для каж­ дого объекта Person будет выделено после считывания данных этого объекта из входного файла.

Person* data [1000];

/ / массив указателей

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

ifstream from( "univ.dat");

/ /

файл входных данных

i f (!from) { cout « " Cannot open f i l e \ n " ;

return

0; }

Для каждого объекта входного файла программа динамически выделяет струк­ туру, а затем считывает четыре элемента данных: строку, определяющую тип объекта, идентификатор, имя и либо служебное положение (для преподавателей), либо специализацию (для студентов). Программа проверяет значение строки, определяюш,ей тип объекта ("FACULTY" или "STUDENT"), и устанавливает поле типа объекта либо в 1, либо в 2.

char buf[80];

// буфер входных данных

Person *р = new Person;

// выделение памяти для нового объекта

from.getline(buf,80) ;

// распознавание поступающего типа

i f (strcmp(buf, "FACULTY")

0)

p->kind = 1;

// 1 для преподавателей

Глава 15 • Виртуальные функции и использование наследования

673

else i f (strcmpCbuf,

"STUDENT")

 

 

p->kincl

= 2;

/ /

2 для студента

 

else

 

/ /

тип не известен

 

p->kincl

= 0;

 

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

f rom.getline(p->icl, 10);

 

 

/ /

считывание

id

 

 

 

f ro(T].getline(buf ,80);

 

 

 

/ /

считывание

имени

 

 

 

p->name = new char[strlen(buf)+1];

/ /

выделение памяти

 

 

 

strcpy(p->name,

buf);

 

 

 

/ /

копирование имени

 

 

 

from.getline(buf,80);

 

 

/ / чтение служебное

положение/специализация

i f (p->kincl

== 1)

 

 

 

/ /

память для служебного

положения

{

p->rank

- new char[strlen(buf)+1];

 

strcpy(p->rank, buf); }

 

 

/ /

копирование служебного положения

else

i f (p->kincl

== 2)

 

 

 

 

 

 

 

 

 

 

{

p->major

= new char[strlen(buf)+1]

 

/ / память для специализации

 

strcpy(p->major,

buf); }

 

 

/ /

копирование специализации

 

 

 

 

 

 

 

ДИНАМИЧЕСКИ РАСПРЕДЕЛЯЕМАЯ

 

 

ПАМЯТЬ СТЕКА

 

 

 

ОБЛАСТЬ ПАМЯТИ

 

 

 

 

 

 

ВИД

 

 

1

 

 

 

 

 

 

 

 

data[0]

1 идентификатор

W

U12345678

 

 

 

 

 

 

 

 

имя

 

^

 

 

W

\

Smith, John

|

 

 

 

 

 

 

 

 

 

 

 

должность

 

 

 

W

 

Associate Professor

|

 

 

 

вид

 

 

2

 

 

 

 

 

 

 

 

data[1]

 

идентификатор

W

U12345611

 

 

 

 

 

 

 

 

имя

 

' W

 

 

W

 

Jones, Jan

 

|

 

 

 

 

 

 

 

 

 

 

 

 

 

специализация

 

 

 

^

 

Computer Science

|

 

 

 

вид

 

 

1

 

 

 

 

 

 

 

 

data[2]

 

идентификатор

W

U12345689

 

 

 

 

 

 

 

 

имя

 

^

 

 

W

Blacl<, Jeanne

|

 

 

 

 

 

 

 

 

 

 

 

должность

 

 

 

W

Assistant Professor

|

 

 

 

вид

 

 

2

 

 

 

 

 

 

 

 

Ня!яГЯ1

 

идентификатор

W

и12345622

 

Green, James

|

 

 

 

 

имя

 

 

 

 

W

 

 

 

 

специализация

 

 

 

W\

Astronomy

 

 

1

 

Рис. 15.10. CinpyKrnypa динамически

распределенной

памягпи

 

 

 

 

для

входных

данных

 

 

 

 

 

 

 

На рис. 15.10 представлена структура элемента данных для этого примера. Массив clata[ ] в левой части рисунка является стековым массивом, а вся осталь­ ная память справа от массива (объекты типа Person и их динамическая память) выделяются из динамически распределяемой области памяти.

Хорошей идеей считается инкапсуляция алгоритма считывания в функцию, на­ пример read О, чтобы клиентская программа передавала этой функции файловый

674

Часть iV • Расширенное использование С'^-^-

 

 

 

объект и указатель Person. Функция read() должна выделять память для объекта

 

Person, считывать данные из файла и заполнять объект Person входными данными.

 

Person*

data[20];

int

cnt = 0;

 

/ /

массив указателей

 

ifSt ream fromC'univ.dat");

 

 

/ /

входной файл: библиотечный объект

 

i f

(!from)

{

cout

«

"

Cannot open

f i l e \ n " ;

return 0; }

 

 

while

(! f rom.eof 0 )

 

 

 

 

/ /

считывание до eof

 

{

read(from,

data[cnt]);

 

 

/ /

data[cnt] имеют тип

 

Person*

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cnt++;

}

 

 

 

 

 

 

 

 

 

 

 

 

cout

«

 

" Total records read: " «

cnt «

endl «

endl;

 

 

 

Соберем функцию

read()

из частей, описанных ранее. У этой функции есть

 

два главных недостатка, которые относятся к передаче параметра.

 

void

read

(ifstream

f,

Person* person)

 

/ /

плохой интерфейс

 

{

char

buf[80];

 

 

 

 

 

 

 

 

 

 

 

Person*

p - new Person;

 

/ /

выделение памяти для нового объекта

 

 

f.getline(buf,80);

 

 

 

 

 

/ /

распознавание входного типа

 

 

i f

(strcmp(buf,

"FACULTY")

== 0)

 

 

/ /

1 для

преподавателей

 

 

 

p->kind

= 1;

 

 

 

 

 

 

 

 

else

i f

 

(strcmp(buf.

"STUDENT")

== 0)

 

 

 

 

 

 

 

p->kind

= 2 ;

 

 

 

 

 

 

/ /

2 для

студента

 

 

else

 

 

 

 

 

 

 

 

 

 

/ /

тип неизвестен

 

 

 

p->kind

= 0 ;

 

 

 

 

 

 

 

 

f.getline(p->id,10);

 

 

 

 

/ /

считывание идентификатора

 

 

f.getline(buf,80);

 

 

 

 

 

/ /

считывание имени

 

P->name = new char[strlen(buf)+l];

 

/ /

выделение памяти

 

strcpy(p->name,

buf);

 

 

 

/ /

копирование имени

 

f.getline(buf,80)

;

/ / считывание служебного положения/специализации

 

i f

(p->kind == 1)

 

 

 

 

 

 

 

 

 

 

 

{

p->rank = new char[strlen(buf)+l];

/ / память для служебного положения

 

 

 

 

strcpy(p->rank,

buf);

}

 

/ /

копирование служебного положения

 

else

i f

(p->kind

== 2)

 

 

 

 

 

 

 

 

 

{

p->major = new char[strlen(buf)+l];

/ /

память для специализации

 

 

 

 

strcpy(p->major, buf); }

 

 

/ /

копирование специализации

 

person

= p;

}

 

 

 

 

 

 

/ /

присоединение к массиву

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

void read (ifstream& f, Person*

person)

/ / считывание одной записи

{ char buf[80] ;

 

 

Person* p = new Person;

/ /

выделение памяти для нового объекта

person=p; }

/ /

остальная часть readO

/ /

присоединение нового объекта

Рекомендуем помечать ссылки на объект как константы, если функция не из­ меняет состояние объекта во время ее выполнения. В данном примере модифика­ тор const отсутствует, потому что при считывании информации из файла файловый объект изменяется. Программист серверного класса if st ream клиентской части не должен проектировать серверный класс. Чтобы передать операции ввода/вывода, ему достаточно знать, что состояние файлового объекта изменяется.

Глава 15 • Виртуальные функции и использование наследования

| 675 |

Теперь опишем параметр-указатель. Если в C++ параметр передается указа­ телем (или по ссылке), то его значение может изменяться в пределах функции и оно будет оказывать влияние на фактический аргумент в клиентском простран­ стве. Утверждение о том, что "параметр передается по указателю", должно вос­ приниматься серьезно. Оно не обозначает "параметр-указатель" (именно это чаще всего запутывает программистов). В варианте функции readO указатель на неко­ торое лицо Person передается по значению. Следовательно, его значение не может изменяться при вызове функции. Если до вызова функции параметр-указатель был направлен в никуда, он будет указывать в неопределенном направлении и после вызова функции, а не на выделенный объект Person.

Здесь с помощью указателя передается объект Person. Действительно, если объект Person надлежащим образом передается в эту функцию, он будет правиль­ но заполнен данными входного файла.

Person person;

/ /

объект

Person, не указатель

read(from, &person);

/ /

объект

передается по указателю

"Надлежащая" передача означает передачу адреса объекта. Такой объект су­ ществует в клиентском пространстве. Передача объекта по указателю позволяет программе изменять ее при вызове функции. Даже в этом случае функция read() сталкивается с проблемой: переменная person теперь имеет тип Person, а у пере­ менной р, используемой в последней строке read(), есть тип Person*. Эти два типа очень похожи, однако они немного различаются. Они относятся к разным типам. Один является классом со всеми своими членами, другой — указателем на объект класса. Это можно зафиксировать, если необходимо. Однако это оставило бы нас в клиентском пространстве с массивом объектов Person вместо массива указателей Person.

Вам надо зафиксировать функцию read() для уверенности, что вся память динамически распределяемой области, выделенная в read(), правильно присоеди­ нена к памяти стека, показанной на рис. 15.10.

В момент обращения функции к read() объекта Person еще не было. Теперь он выделяется в функции. Это указатель на объект Person, который существует в клиентском пространстве (весь массив указателей в целом). Указатель, который передается функции read().

while (! f rom.eofO)

/ /

считывание до eof

{ read(from, data[cnt]);

/ /

data[cnt] типа Person*

cnt++; }

 

 

Перед вызовом фактический аргумент, указатель Person, содержит "мусор", поскольку он является частью массива динамически распределяемой области па­ мяти. После вызова указатель ссылается на объект Person, выделенный в функции readO, как показано на рис. 15.10. Следовательно, он содержит действительный адрес участка верхней части памяти и его содержимое изменяется после вызова. Затем указатель должен передаваться либо по ссылке, либо по указателю. Именно поэтому вариант read(), представленный выше, неверен.

Убедитесь, что вы не запутались в этом нагромо>вдении терминов: указатели на объекты, ссылки на указатели, указатели на указатели и т. д. Помните, что указатель является обычной переменной, которую можно передать по значению, по ссылке либо по указателю. Это просто неудачная запись в нотации С+ + (унаследованная от С), допускающая две интерпретации интерфейса функции.

void read

(ifstreanf)& f, Person*

person)

/ / считывание одной записи

{ char buf[80] ;

 

 

 

Person*

p = new Person;

/ /

выделение

памяти для нового объекта

. . .

 

/ /

остальная

часть read()

person = р; }

/ /

присоединение нового объекта

IT 676

Часть IV # Расширенное использование С-^-^

Здесь Person* person можно интерпретировать либо как объект Person, переда­ ваемый по указателю, либо как указатель Person (типа Person*), передаваемый по значению. Передать этот указатель по ссылке совсем не сложно. Стандартное правило C + + (описанное в главе 7 "Программирование с использованием функций C++") указывает, что для перехода от передачи по значению к передаче по ссылке необходимо только вставить амперсанд (&) между наименованием типа и именем параметра. Другие изменения не нужны, ни в теле функции, ни в син­ таксисе вызова функции. Вот так он должен выглядеть.

void read (ifstream& f, Person* &person)

/ / считывание одной записи

{ char but[80] ;

 

 

Person*

р = new Person;

/ /

выделение памяти для нового объекта

 

 

/ /

остальная часть read()

person

= р; }

/ /

присоединение нового объекта

Чтобы облегчить этот переход, параметру было первоначально присвоено имя Person* person, а не Person *person. Но это не играет роли. C + + не учитывает пробелы в этом случае, и можно расположить звездочку (и амперсанд) между наименованием типа и именем параметра наиболее подходящим способом.

Передача указателя по указателю не является проблемой. Необходимо быть осторожным при вызове функции, интерфейсе функции и в теле функции. В вызо­ ве функции звездочка вставляется перед именем переменной (т. е. перед именем указателя data[cnt]). Вызов функции выглядит так:

while (! f rom.eofO)

/ /

считывание до eof

{ read(from, &data[cnt]);

/ /

передача указателя по указателю

cnt++; }

 

 

Винтерфейсе функции звездочка вставляется перед наименованием Парамет­ ра, т. е. вместо Person* person следует указывать Person* *person (или Person** person).

Втеле функции, и именно здесь чаще всего возникают ошибки, звездочку следует использовать перед именем параметра. Имя параметра — person, а не *person или **person. Следовательно, последний оператор в функции read() должен использовать разыменование:

void read

(ifstream& f, Person^

person)

/ /

указатель no указателю

{ char buf[80];

 

 

 

Person

*p = new Person;

/ /

выделение памяти для нового объекта

*person

= p; }

/ /

остальная

часть readO

/ /

приз!

 

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

До сих пор обсуждались технические детали ввода данных в массив внутри компьютера. Мы не рассматривали динамическое связывание, полиморфизм и другие вопросы. Мы считаем, что повторение материала из 6 и 7 глав по дина­ мическому управлению памятью, файлам ввода/вывода и передаче параметров пойдет только на пользу.

Динамическое связывание становится проблемой, когда программа начинает обработку данных, которые уже находятся в памяти. Объекты разных видов тре­ буют разной обработки, поэтому программе необходимо распознавать в каждом конкретном вызове, из какой области обрабатывается объект. Здесь пригодится поле kind класса Person. В этом простом примере "обработка данных" означает тщательный просмотр массива указателей и вывод на печать каждого объекта Person либо как преподавателя (с отображенным rank — служебным положением), либо как студента (с отображенной major — специализацией). В реальной жизни

Глава 15 • Виртуальные функции и использование наследования

677

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

for (int i=0; i < cnt; i++)

{ cout « "

id: " «clata[i]->icl «endl;

cout « "

name: " «data [i]->name «endl ;

i f (data[i]->kind == 1)

cout « " rank: " «data[i]->rank «endl;

else i f

(data[i]->kind == 2)

cout « " major: " «data[i]->major «endl;

cout «

endl; }

/ /

просмотр массива указателей

/ /

вывод на печать

 

/ /

иденти(11икатора,

имени

 

/ /

должность на факультете

 

/ /

специализация

студента

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

В листинге 15.3 приведена программа, обрабатывающая входной файл (см. рис. 15.9). Дополнительно к типу Person и функции read() программа содержит функцию main(), выполняющую роль клиентской программы. Она определяет массив указателей и файловых объектов, считывает и обрабатывает в цикле вводимые данные, а затем воз­ вращает динамическую память. Результаты выполнения программы представлены на рис. 15.11.

Total records read: 4

Id: U12345678 name: Smith, John

rank: Associate Professor

Id: U12345611 name: Jones, Jan

major: Computer Science

Id: U12345689 name: Black, Jeanne

rank: Assistant Professor

Id: U12345622 name: Green, James major; Astronomy

Рис. 15.11.

Вывод результатов обработки неоднородного списка объектное

Листинг 15.3. Обработка неоднородного списка — традиционный подход

#include <iostream> #inclucle <fstream> using namespace std;

struct Person { int kind;

char id[10]; char* name; char* rank; char* major;

} ;

void read (ifstream& f, Person*& person)

{char buf[80];

Person* p = new Person; f.getline(buf,80);

i f (strcmp(buf, "FACULTY") == 0) p->kind = 1;

else if (strcmp(buf, "STUDENT") == 0) p->kind = 2;

else

p->kind = 0; f.getline(p->id,10); f.getline(buf,80);

p->name = new char[strlen(buf)+1];

//1 для преподавателей, 2 для студентов

//фиксированной длины

//переменной длины

//только для преподавателей

//только для студентов

//считывание одной записи

//выделение памяти для нового объекта

//распознавание входящего типа

//1 для преподавателей

//2 для студентов

//тип неизвестен

//считывание идентификатора

//считывание имени

//выделение памяти

I

678 ^

 

 

Чость IV ^ Расширенное использование C-^-^

 

strcpy(p->name,

buf);

 

 

 

 

 

// копирование имени

 

f.getline(buf.80);

 

 

 

 

 

 

// считывание rank/major

 

i f (p->kincj == 1)

 

 

 

 

 

 

// память для rank

 

{ p->rank = new char[strlen(buf)+1];

 

 

 

strcpy(p->rank, buf); }

 

 

 

 

// копирование rank

 

else if (p->kind == 2)

 

 

 

 

// память для major

 

{ p->major = new char[strlen(buf)+1];

 

 

 

strcpy(p->major, buf); }

 

 

 

 

// копирование major

 

person = p;

 

 

 

 

 

 

 

 

// присоединение к массиву

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

int mainO

 

 

 

 

 

 

 

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

 

// массив указателей

 

Person* clata[20]; int cnt = 0;

 

 

 

 

 

ifSt ream fromC'univ.dat");

 

 

 

 

// файл входных данных

 

if (!from) {cout «

" Cannot open file\n"; return 0; }

 

while

(Ifrom.eofO)

 

 

 

 

 

 

 

 

{ read

(from,

data[cnt]);

 

 

 

 

/ / считывание до eof

 

cnt++;

}

 

 

 

 

 

 

 

 

 

 

 

cout

«

 

" Total

records read: "

«

cnt

«

endl «

endl;

 

for

(int

i=0;

i

< cnt;

i++)

 

 

 

 

/ /

вывод на печать идентификатора, имени

 

{ cout

« "

i d :

"

« d a t a [ i ] - > i d

« e n d l ;

 

 

 

cout

« "

name: " «data[i]->name

« e n d l ;

 

 

 

i f

(data[i]->kind

== 1)

 

 

 

 

/ /

должность на факультете

 

 

cout

« "

rank: "

«data[i] - >rank

« e n d l ;

 

else

i f

 

(data[i]->kind == 2)

 

 

 

 

/ /

специализация студента

 

 

cout

« "

major:

"

«data[i]->major

« e n d l

 

cout

«

 

endl;

}

 

 

 

 

 

 

 

 

 

for

(int

j=0;

j

< cnt;

j++)

 

 

 

 

 

 

 

{ delete

[ ] data[j]->name;

 

 

 

 

/ /

удаление имени

 

i f

(data[j]->kind

== 1)

 

 

 

 

 

 

 

 

delete [ ]

data[j]->rank;

 

 

 

 

/ /

удаление rank/major

 

else

i f

 

(data[j]->kind == 1)

 

 

 

 

 

 

 

 

delete [ ]

data[j]->major;

 

 

 

 

 

 

 

delete

data[j]; }

 

 

 

 

 

 

/ /

удаление записи

 

return

0 ;

 

 

 

 

 

 

 

 

 

 

 

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

В этом решении нет ничего сложного или запутанного (за исключением, возмож­ но, записи указателя). Хотя здесь и используется много ловушек языка С+ + (например, структуры, указатели, динамическое управление памятью в операто­ рах new и delete, передача параметра по ссылке, библиотечные файловые объек­ ты), подобную программу можно написать на любом языке. В ней реализовано динамическое связывание (каждый объект обрабатывается в соответствии с его собственным типом). Однако преимуш.ества объектно-ориентированных возмож­ ностей языка не используются (например, связывание данных и операций вместе, конструкторы и деструкторы, передача обязанностей серверам, наследование и т. д.).

Динамическое связывание: объектно-ориентированный подход

вследуюш,ем ниже варианте программы создадим три класса: Person, Faculty

иStudent. Все возможности, обш^е для обработки данных преподавателей и сту­ дентов, перейдут в базовый класс Person — поля id, name и kind. Вместо исполь­ зования чисел для обозначения типа объекта (1 — для преподавателей, 2 — для

Глава 15 • Виртуальные функции и использование наследования

| 679 щ

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

Элементы данных базового класса Person определены как защищенные, а не как закрытые. Производные классы Faculty и Student смогут осуществлять доступ к этим элементам данных.

struct Person {

 

 

public:

 

 

 

enum Kind { FACULTY, STUDENT } ;

 

 

protected:

 

 

Kind

kind;

/ /

FACULTY или STUDENT

char

id[10];

/ /

данные общие для обоих типов

char*

name;

/ /

переменной длины

public:

Person(const char i d [ ] , const char nm[], Kind type); Kind getKindO const;

-PersonO; } ;

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

Другая функция-член getKindO является вспомогательной. Это сообщение будет отправлено клиентской программе (функция read()) для вычисления объек­ тов производных классов (Faculty и Student). В предыдущем варианте функция read() осуществляла непосредственный доступ к полю kind, создавая, таким обра­ зом, зависимость между различными частями программы. В данной структуре поле kind является защищенным, не закрытым, а класс Person должен предоставить функцию доступа для обслуживания клиентов своих производных классов.

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

Производные классы Faculty и Student наследуются из Person открыто. Не­ смотря на то, что для их определения используется зарезервированное слово struct, режим наследования определяется явно, во избежание путаницы. Если бы режим был пропущен, он был бы общедоступным по умолчанию. Общедоступный режим является наиболее естественным и удобным, он не запрещает клиентам произ­ водных классов использование возможностей базового класса. В данном случае это не важно. Базовый класс настолько мал, что рекомендуем вам использовать метод getKindO.

Тем не менее использование общедоступного наследования имеет большое значение. Здесь применяется массив указателей типа Person, но предполагается установить эти указатели на объекты классов Faculty и Student. Для этого ис­ пользуется приведение типов. Чтобы неявное приведение стало возможным, в С+Н- должен быть общедоступный режим наследования.

При необходимости можно использовать и явное приведение. Но есть и другой, более важный вопрос. По существу в этой структуре предполагается использовать виртуальные функции. Они позволяют клиентской программе вызывать метод производного класса, например write(), и разрешают среде выполнения про­ грамм определять, к которому из производных классов принадлежит эта функция.

Соседние файлы в предмете Программирование на C++