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

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

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

320

 

 

 

Часть II • Объектно-ориентированное програ1^1Уирование но C-^-f

Листинг 8.7.

Пример использования функций доступа для изолирования клиента

 

 

 

 

 

от имен полей данных

 

 

 

#inclucle

<iostream>

 

 

 

 

 

/ /

инкапсуляция в серверных функциях

using

namespace std;

 

 

 

 

 

 

struct

Cylinder

{

 

 

 

 

 

/ /

структура данных для доступа

 

double

radius,

height;

}

;

 

 

 

void enterData(Cylinder &c,

char number[])

 

 

 

{

cout

«

"Введите радиус и высоту ";

 

 

 

 

cout

«

number «

"

цилиндра: ";

 

// инициализация цилиндра

 

cin

»

с. radius »

с.height;

}

 

void validateCylinder(Cylinder

c)

 

// значения поумолчанию для данных

{

i f

(c.radius

< 0)

с

radius = 10;

 

 

i f

(c. height

< 0)

с height = 20; }

 

 

 

double getVolume(const

Cylinder &с)

 

// вычислить объемы

{

return

с.height *

с. radius

*

с. radius 3.141593;

 

 

 

void scaleCylinder(Cylinder &c, double factor)

 

// масштабирование размеров

{ с radius *=factor; c.height

*=factor; }

 

void printCylinder(const Cylinder &c)

 

// печать состояния объекта

{ cout «

"радиус:

«

с. radius « " высота: " «

с. height « endl;}

int mainO

 

 

 

 

 

 

 

 

 

 

Cylinder с1,

с2;

 

 

 

 

 

// данные программы

 

enterData(c1,

"первого");

 

 

 

// инициализация первого цилиндра

 

validateCylinder(c1);

 

 

 

// по умолчанию на случай порчи данных

 

enterData(c2,

"второго");

 

 

 

// инициализация второго цилиндра

 

validateCylinder(c2);

 

 

 

// по умолчанию на случай порчи данных

 

if (getVolume(cl) < getVolume(c2))

 

// сравнить объемы

 

{ scaleCylinder(c1,1.2);

 

 

// масштабировать

 

 

cout « "\пИзмененный размер первого цилиндра\п";

// вывод нового размера

 

printCylinder(cl); }

 

 

 

 

 

 

else

 

 

"\пРа'змер первого цилиндра не изменен"

«

endl;

 

 

cout «

 

 

return 0;

 

 

 

 

 

 

 

 

 

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

Преимущество такого подхода в том, что образуются две разные области: одна относится к проектированию определяемого программистом типа Cylinder и его доступа к функциям, а другая — к клиентскому коду, который использует объекты Cylinder и вызывает функции доступа Cylinder. При традиционном программиро­ вании (как в листинге 8.6) таких разделенных областей нет. Если имена полей определенной программистом структуры типа Cylinder изменяются, то придется проверять весь код, т. к. эти имена могут использоваться в любом месте програм­ мы. В новом варианте (как в листинге 8.7) изменение *в именах полей Cylinder повлияет только на функции доступа — хорошо определенный набор функций.

.Остальная часть программы (а она может быть очень большой) не затрагивается.

Глава 8 • Программирование с использованием функций

с321

 

 

main()

 

 

enterDataO

vaidateCylinderO

firstlsSmaller()

scaleCylinderO

printCylinder()

Рис. 8.7. Структурная диаграмма для программы из лист^инга 8.7

Рис. 8.7 иллюстрирует эту взаимосвязь клиентской и серверной части в виде струк­ турной диаграммы. Клиент main() вызывает серверные функции, обращающиеся к полям объектов Cylinder. Эти серверные функции инкапсулируют функциюклиент от деталей структуры Cylinder.

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

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

Эти идеи насчет защиты данных передаются среди программистов из поколе­ ния в поколение. Звучит просто и разумно. Легче принять их, чем идти против общего мнения. Здесь нужно возразить. Хотя защита данных действительно играет здесь некую роль, но весьма небольшую. Инкапсуляция данных — прежде всего удобство чтения исходного кода и независимость компонентов программы. Что

исоставляет основную тему главы.

Вдействительности передача параметров не защищает переменные. Если кто-то ошибочно думает, что переменной нужно присвоить новое значение, это можно сделать с помощью прямого присваивания (если переменная глобальная) или присваивания значения параметру (если она передается как ссылка или параметруказатель). Аналогично, если кто-то ошибочно полагает, что полю с1. radius сле­ дует присвоить новое значение, то это также можно сделать с помощью прямого присваивания (если не используется инкапсуляция) или вызвать функцию доступа, например, setCylinder(), когда инкапсуляция применяется. Разницы нет.

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

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

322 I Чость I! # Объектно-ориентировонное г1рогра1^1ллирование на С^--^

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

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

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

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

Сокрытие информации

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

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

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

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

Чем сокрытие информации отличается от инкапсуляции? Перед ответом на данный вопрос давайте рассмотрим не очень эффективный пример инкапсуляции. Попробуем реализовать инкапсуляцию, введя функции-серверы, выполняющие операции с объектом Cylinder, например возвращающие значения полей Cylinder

Глава 8 • Программирование с использованием функций

323

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

void setRadius(Cylinder

&с,

double

г)

/ /

функция-модификатор

{ с.radius = г;

}

 

 

 

 

 

 

void setHeight(Cylinder

&с,

double

h)

/ /

функция-модификатор

{ с.height = h;

}

 

 

 

 

 

 

double getRadius(const

Cylinder& c)

/ /

функция-селектор

{ return c.radius;

}

 

 

 

 

 

double getHeight

(const

Cylinder&

c)

/ /

функция-селектор

{ return с height;

}

 

 

 

 

 

Функция mainO не обязана использовать имена компонентов цилиндра. Если имена изменяются, то изменять придется функции setRadiusO, setHeightO, getRadiusO и getHeightO, а не main() или других клиентов Cylinder. Пример использования этих функций доступа показан в листинге 8.8. Результат данной программы будет тем же, что и у программы из листинга 8.6. Функциональность ее осталась той же.

Листинг 8.8. Пример неэффективной инкапсуляции

«include

<iostream>

 

 

 

 

// неуклюжая инкапсуляция

using

namespace std;

 

 

 

 

 

 

struct

Cylinder

{

 

 

 

 

 

// структура данных для доступа

 

double

radius,

height; }

;

 

 

 

 

void setRadius(Cylinder

&c,

double

r)

// модификатор функции

{

c.radius = r;

}

 

 

 

 

 

 

 

void setHeight(Cylinder

&c,

double

h)

// модификатор функции

{

c.height = h;

}

 

 

 

 

 

 

 

double getRadius(const

Cylinders

c)

 

// селектор

функции

{

return

c.radius;

}

 

 

 

 

 

 

double getHeight

(const

Cylinders

c)

// селектор

функции

{

return

c.height;

}

 

 

 

 

 

 

int mainO

 

 

 

 

 

 

 

 

{

Cylinder c1, c2; double radius, height;

// данные программы

 

 

cout «

"Введите радиус и высоту первого цилиндра:

 

 

cin »

radius »

height;

 

 

 

// инициализация данных

 

setRadius(c1,radius); setHeight(c1,height);

 

 

if (getRadius(c1)<0) setRadius(c1,10);

// проверка данных

 

if (getHeight(c1)<0) setHeight(c1,20);

цилиндра:

 

 

cout «

"Введите радиус и высоту второго

 

 

cin »

radius »

height;

 

 

 

// инициализация данных

 

setRadius(c2,radius); setHeight(c2,heihgt);

 

 

if (getRadius(c2)<0) setRadius(c2,10);

// проверка данных

 

if (getHeight

(c2)<0) setHeight(c2,20);

 

 

324

Часть il # Объектно-ориентированное nporpammmpci

if (getHeight(c1)*getRaclius(c1)*getRaclius(c1)*3.141593

<getHeight(c2)*getRaclius(c2)*getRadius(c2)*3.141593)

{setRaclius(c1,getRadius(c1)*1.2); setHeight(c1,getHeight(c1)*1. 2);

cout « "\пИзмененный размер первого цилиндра\п";

cout « "радиус: "«с1. radius«" высота: "«с1. height«endl;

else

cout « "Размер первого цилиндра неизменен" « endl; return 0;

u-f-t-

//масштабирование

//вывод нового размера

 

 

 

 

 

Как видно, функция main() действительно ин­

 

 

setRadiusO

 

капсулирована от имен полей данных Cylinder.

radius ^

 

1

 

Если в ходе перепроектирования эти имена из­

getRadiusO

main()

менятся, то придется модифицировать ограничен­

height

w^^*>>^

 

1

ный и легко идентифицируемый набор функций.

 

setHeightO

 

Никаких других частей программы, даже если

 

\

 

 

 

i

 

она очень большая, изменять и даже проверять

 

\\

 

 

 

getHeightO 1

 

не нужно. Конечно, придется ее перекомпилиро­

 

 

 

 

 

 

 

 

 

 

вать, но это уже совсем другая история. Рис. 8.8

 

 

 

 

 

демонстрирует диаграмму объектов для такой

Рис. 8 . 8 . Диаграмма

объектов

 

архитектуры. Подобно объектам из диаграммы,

 

приведенной в главе 1 (рис. 1.7), эта диаграмма

 

из листинга

8.8

 

демонстрирует, что функции-серверы setRadiusO,

 

 

 

 

 

 

 

 

 

 

setHeightO, getRadiusO и getHeight() концеп­

 

 

туально родственны. Они обращаются к полям данных структуры Cylinder —

 

 

height и radius — от лица клиента. Клиент имеет доступ к данным сервера только

 

 

через вызовы функций доступа, а не непосредственно.

 

 

 

Между тем инкапсуляция здесь достаточно неуклюжая и на самом деле беспо­

 

 

лезная. Не используются архитектурные принципы, перечисленные в начале гла­

 

 

вы. Функции доступа очень мало делают для достижения целей клиентского кода.

 

 

Ответственность за

операции

с данными не переносится на функции-серверы,

 

 

а остается за клиентом. Несмотря на использование функций доступа, в клиенте

 

 

mainO смешивается доступ к данным, например, вызовы функций getRadiusO,

 

 

с операциями сданными, так что смысл вычислений (вычисление объема, измене­

 

 

ние размера) уяснить нелегко. Если изменится число полей определенного про­

 

 

граммистом типа Cylinder, то изменится и число функций доступа, а в результате

 

 

потребуется модифицировать код клиента.

Для корректного выбора набора серверных функций нужно принимать во вни­ мание обязанности клиента. В данном примере клиент отвечает за инициализа­ цию объектов Cylinder, проверку данных объекта, вычисление объема цилиндра, масштабирование его размера и вывод на экран атрибутов цилиндра. Давайте создадим соответствующие функции доступа: setCylinder(), validateCylinder(), getVolumeO, scaleCylinder() и printCylinder().

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

Таким образом, операции с данными уже не смешиваются с доступом к ним. В клиенте определяется, что будет сделано (присваивание значений полям, вы­ числение объема и т. д.), а в серверном коде — как это делается. Представление данных Cylinder инкапсулировано. Если имена полей изменятся, то на клиента

Глава 8 « Программирование с использованием функций

325

это не повлияет. Если к Cylinder добавляются поля данных, на клиенте такое изменение не отразится. (На самом деле, это не совсем так, поскольку операции ввода также нужно инкапсулировать.)

Информация, передаваемая от разработчика клиентской части, ограничивается именами и интерфейсами серверных функций. Зоны ответственности разработчи­ ков клиентской и серверной части разделены: одна охватывает связанные с прило­ жением операции высокого уровня, а другая ограничивается именами полей данных и вычислениями нижнего уровня.

Даже в этом небольшом примере видны преимущества применения функций доступа. Код клиента выражается в терминах осмысленных операций уровня при­ ложения. Что означает с1. heihgt*c1. raclius*c1. raclius*3.141593 в листинге 8.6? Программисту, сопровождающему приложение, придется это выяснить. То же са­ мое относится к операторам cl/radius* = 1.2; и с1. height* = 1.2;. Изменяются ли все размеры цилиндра? Применяется ли ко всем размерам один и тот же коэф­ фициент? Отображают ли операторы вывода все размеры цилиндра или только некоторые? Когда доступ к данным комбинируется с операциями приложения, смысл обработки уяснить труднее.

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

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

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

Что же добавляет сокрытие информации к инкапсуляции? Давайте снова рас­ смотрим серверные функции validateCylinderO и getVolume(). Первая функция инкапсулирует операции проверки данных, значения по умолчанию и т. д. Это хорошо, поскольку клиентскому коду не нужно знать всех деталей проверки до­ пустимости данных. Достаточно, что она выполняется. Вторая функция инкапсу­ лирует геометрические вычисления. Это тоже хорошо, потому что в клиентской программе можно не беспокоиться о правилах геометрии. Достаточно знать, что вычисляется объем цилиндра.

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

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

8.9. Диаграмма объектов в листинге 8.9

326

Часть II • Объектно-ориентированное программирование на C-^-t-

перепроектирования, т. е. изменения списка функций и их обязанностей. Хоришим решением этой проблемы будет объединение функций validateCylinderO и enterDataO:

void enterDate(Cylinder &с, char number[])

{cout « "Введите радиус и высоту "; cout «number « " цилиндра: ";

cin

» с.radius »

с. height;

/ /

инициализировать цилиндр

i f

(с. radius

< 0)

с. radius

= 10;

/ /

значения по умолчанию для

 

 

 

 

 

/ /

запорченных данных

i f

(с.height

< 0)

с.height

= 20;

}

 

radius height

Рис.

Как можно вновь и вновь убеждаться, критерии сцепления, связности, инкап­ суляции и сокрытия информации лишь указывают на наличие промахов в архитек­ туре программы, но не говорят, в каком направлении следует двигаться, как изменить программу, чтобы устранить недостаток. Принципы, перечисленные в начале главы,— это рабочие правила. Они показывают, как нужно изменить архитектуру программы. В данном примере сокрытие информации улучшается путем переноса обязанностей на функции-серверы. Вместо того чтобы вынуждать клиента вызывать две серверных функции, enterDataO и validateCylinderO, данная конструкция требует от клиента-вызова только одной функции доступа.

Функция getVolumeO нарушает принцип переноса обязанностей на серверные функции, предоставляя клиентской программе больше информации, чем ей нуж­ но. Клиенту необходимо знать только, что один цилиндр больше другого. Вместо обслуживания потребностей клиента серверный код возвраш,ает вычисленное значение объема и позволяет клиенту делать с ним все, что тому заблагорассу­ дится. Информацию об объеме цилиндра следует скрыть от клиентского кода. Для этого надо изменить архитектуру программы, введя, например, функцию firstlsSmallerO:

bool

firstIsSmaller(const cylinder& c1, const Cylinder&

c2)

{ i f

(c1.heihgt*c1.radius*c1.radius*3.141593

/ / сравнение объемов

 

< c2.heihgt*c2.radius*c2.radius*3.141593)

 

 

return

true;

 

else

 

 

 

return

false; }

 

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

всех версиях данной программы

функциональность остается одной и той же.

 

 

 

Изменяется только архитектура, и именно она

 

 

 

влияет на качество программы. Результат про­

enterDataO

 

 

граммы будет тем же, что и у программы из

I

 

 

листинга 8.6.

firstlsSmallerO

mainO

На рис. 8,9 показана диаграмма объектов

I

Z

для данной программы. Видно, что, подобно

scaieCyilnderO

предыдуш,ему рисунку, функции enterDataO,

I

firstlsSmallerO, scaleCylinderO и функция

printCylinderO

printCylinder() родственны (относятся к одной

 

 

категории). Сервер и клиент оформлены лучше,

 

так как функции доступа выполняют работу для

 

клиента, а не просто передают ему информа­

 

цию для дальнейших операций.

Глава 8 • Программирование с использованием функций

327 1

Листинг 8.9. Комбинирование инкапсуляции и сокрытия информации

#inclucle <iostream> using namespace std;

struct Cylinder {

 

// структура данных для доступа

double

radius, height; } ;

 

 

void enterDate(Cylinder &c, char number[])

 

 

{ cout «

"Введите радиус и высоту ";

 

 

cout «

 

number «

" цилиндра: ";

// инициализировать цилиндр

cin »

с. radius »

с.height;

if (с. radius < 0) с.radius =10;

// значения по умолчанию

if (c.height < 0) с height = 20; }

 

 

boolfirstIsSmaller(const cyliTider& c1, const Cylinder& c2)

// сравнение объемов

{ if (c1.heihgt*c1.radius*c1.radius*3.141593

<

c2.heihgt*c2.radius*c2.radius*3.141593)

 

 

return true;

 

 

 

else

 

 

 

 

 

return false; }

 

 

void scaleCylinder(Cylinder &c, double factor)

// масштабирование

размеров

{ c.radius *= factor; c.height *=factor; }

void printCylinder(const Cylinder &с)

// вывод состояния

объекта

{ cout «

 

"радиус: "«с. radius « " высота: "«c.height«endl; }

 

int mainO

{

Cylinder c1, c2; enterData(c1, "первого"); enterData(c2, "второго"); if (firstIsSmaller(c1,c2))

{ scaleCylinder(c1,1.2);

cout « "\пИзмененный размер первого цилиндра\п" printCylinder(cl); }

else

//данные программы

//инициализировать первый цилиндр

//инициализировать второй цилиндр

//масштабирование и

//вывод нового размера

cout « "Размер первого цилиндра неизменен" « endl; return 0;

Большой пример инкапсуляции

Следующий пример — это верификация вводимого выражения. Для простоты его функциональность также ограничена — проверяется лишь парность круглых и квадратных скобок во вводимых выражениях. Рассмотрим функцию checkParenO, поочередно сканирующую символы выражения в завершаюндемся нулем массиве (конец выражения) или определяющую несоответствие скобок. Например, такое математическое выражение а = (x[i] + 5)*у следует признать допустимым, а вы­ ражение а = (x[i] + 5]*у — нет.

Вданном примере будут использованы два глобальных массива — buffer[]

иstore[]. Индекс i считывает символ из массива buffer[], а индекс idx — из

массива store[]. Первоначально флаг valid устанавливается в 1 (true). Если в процессе верификации выражение окажется недопустимым, то названный флаг устанавливается в О (false). Программа в цикле проверяет следующий символ в массиве buffer[]. Если это левая, открывающая скобка (круглая или квадрат­ ная), то решение следует отложить до обнаружения парной закрывающей скобки.

t 328 I Часть !! ^ Объвкто-орыеиттдоваиное г1рогра1М11^ирование на С-^

Для этого программа сохраняет символ (скобку) в массиве store[] и настраивает индекс idx.

char buffer[81]; char store[81]; bool checkParen ()

{char c, sym; inti, idx; bool valid;

i= 0; idx = 0; valid =true;

while (buffer[i] != '\0' &&valid)

{с = buffer[i];

if (c=-(' II С - Ч ' )

{store[idx] = c; idx++; }

//ОСТАЛЬНАЯ ЧАСТЬ ПРОГРАММЫ

return valid; }

//инициализировать данные

//конец данных или ошибка?

//получить следующий символ

//следующая скобка - закрывающая?

//затем сохранить ее

Если следующий символ в массиве buffer[ ] представляет собой закрывающую

скобку (круглую или квадратную), то программа считывает последний символ, со­

храненный в массиве store[](опять настраивая индекс idx). При этом если сим­ вол в массиве buffer[] является правой круглой скобкой, тосимвол в массиве store[] должен быть левой круглой (ане квадратной) скобкой.Если два символа

не соответствуют (являются парными), то ничего делать не надо — программа переходит к парному символу в массиве butfег[ ]. Если два символа не соответст­

вуютдругдругу, товыражение недопустимо. Программа устанавливает флаг valid

в значение false, при этом цикл завершается и клиенту возвращается 0.

char buffer[81]; char store[81]; bool CheckParen ()

*{ char c, sym; inti, idx; bool valid;

i= 0; idx = 0; valid = 1;

while (buffer[i] != '\0' &&valid)

{с = buffer[i];

if (c-'(* II c-'[')

{ store[idx] = c; idx++; } else if (c=='(' I Ic==' ]')

{ idx-; sym = store[idx];

if (!((sym=='C &&c==')') II (sym==4' &&C-']'))) valid = false; }

// ОСТАЛЬНАЯ ЧАСТЬ ПРОГРАММЫ return valid; }

//инициализировать данные

//конец данных илиошибка?

//получить следующий символ

//следующая скобка - открывающая?

//затем сохранить ее

//следующая - закрывающая?

//получить последний символ

//если непарные

//тогда ошибка

Конечно, такой подход слишком оптимистичен. Откуда программа знает, что в массиве store[] всегда есть символ для сравнения с открывающей скобкой из массива buffег[ ]? Если вводимое выражение содержит несколько правых круглых скобок,не соответствующих предшествующим левым скобкам,то массив store[] будет очищен, его индекс станет отрицательным, а выражение следует объявить

недопустимым.

char buffer[81]; char store[81];

 

int CheckParen ()

 

{ Char c, sym; int i, idx; bool valid;

// инициализировать данные

i = 0; idx = 0; valid = 1 ;

while (buffer[i] != '\0' && valid !=0)

// конец данных или ошибка?

{ с = buffer[i];

// получить следующий символ

if (с=='(' IIс=='[')

// следующая скобка - открывающая?

{ store[idx] = с; idx++; }

// затем сохранить ее

Выражение a=(x[i]+5)*y; допустимо
Выражение a=(x[i)+5]*y; недопустимо
Рис. 8.10. Результат выполнения программы из лист^инга 8.10

Глава 8 • Программирование с использованием функций

329

else i f

(с=='('

II с==']')

/ /

следующая - закрывающая?

 

i f

(idx

> 0)

 

 

/ /

существует ли сохраненный символ?

 

{ idx-;

sym = store[idx];

/ /

получить последний символ

 

 

i f

(!(sym=='('

&& с==')')

II

 

 

 

 

(sym=='['

&&с==']')))

 

//если непарные

 

else

 

valid = 0;

}

/ /

тогда ошибка

 

valid

= 0;

/ / если нет парного сохраненного символа,

ошибка

 

// ОСТАЛЬНАЯ ЧАСТЬ ПРОГРАММЫ return valid; }

Вот почти и все. Мы решили, что нужно делать, если следующий символ в мас­ сиве buf fег[ ] является левой скобкой, и что делать в том случае, если это правая скобка. Если же это не правая и не левая скобки, нужно просто перейти к следую­ щему символу в массиве buffer[], т. е. увеличить индекс i.

char buffer[81]; char store[81]; bool checkParen ()

{ char c, sym; int

i, idx;

bool; valid;

i = 0;

idx = 0; valid

= true;

while

(buffer[i]

!=

' \ 0 '

&& valid)

{ с = b u f f e r [ i ] ;

if (c=='(' II c-'[')

{store[idx] = c; idx++; } else if (c=='(' II c==']') if (idx > 0)

{idx-; sym = store[idx];

if (!((sym=='(' &&C-')') II

(sym=='[' &&c==']'))) valid = false; }

else

//инициализировать данные

//конец данных или ошибка?

//получить следующий символ

//следующая скобка - открывающая?

//затем сохранить ее

//следующая - закрывающая?

//существует ли сохраненный символ?

//получить последний символ

//если непарные

//тогда ошибка

 

valid

= false;

/ / если нет парного сохраненного символа, ошибка

i++; }

 

 

/ / перейти к следующему символу

/ / Т О ,

О ЧЕМ НУЖНО ПОБЕСПОКОИТЬСЯ ПОСЛЕ ЗАВЕРШЕНИЯ ЦИКЛА

return

valid;

}

/ / возврат статуса ошибки

В данной программе нужно заботиться еи;е кое о чем. В конце цикла флаг valid устанавливается в значение false, и это значение должно возвращаться в вызывающую программу. Никаких вопросов не задается — введено недопусти­ мое выражение. Однако если флаг сохраняет значение true, программа тоже должна передать эти "хорошие новости" в вызывающую программу. Во-первых, ей следует проверить наличие дополнительных символов, не совпавших с правыми скобками в выражении, которые могли остаться в массиве store[]. В этом случае idx > О, выражение недопустимо, а флаг valid следует установить в значение false.

Функция checkParenO наряду с остальным исходным кодом включена в лис­ тинг 8.10. Число операторов if в данном примере достаточно велико. Это означает, что для демонстрации корректности функции ее потребуется вызывать не один раз, что усложняет тестирование. Специальная тесто­

вая функция checkParenTestO вызывает функцию CheckParenO, показывает введенное выражение и результат выполнения функции. На рис. 8.10 представлен результат выполнения программы.

Подобно предыдущим примерам данной главы, здесь делается попытка инкапсулировать програм­ мный код от представления символов в функции CheckParenO. Этот алгоритм проверки не зависит

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