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

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

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

290

Часть II * Объектно-ориентированное програ1М11^ирование на С^*^

На первый взгляд, это Maj;io что дает. Если нужно сложить 25 компонентов массива, почему бы не сделать это явно? На самом деле, применение используе­ мых по умолчанию значений параметров усложняет программный код, но в неко­ торых случаях, когда функция содержит большое число аргументов и вызывается часто лишь с некоторыми аргументами, а другие включаются в вызов редко, может упростить его. Например, функция getlineO в файле istream. h имеет следуюш,ий прототип:

istream& getline(char buf[], int count, char delimiter = ' \ n ' );

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

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

double

sum(const

double

a [ ] , i n t n)

/ /

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

 

 

 

 

/ /

не используется

{ double t o t a l

= 0.0;

 

 

 

for

(int i=0;

i<n;

i++)

 

 

 

t o t a l += a [ i ] ;

 

 

 

return t o t a l ;

}

 

 

 

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

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

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

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

Поскольку имена параметров в прототипах функции не обязательны, можно "инициализировать" значением по умолчанию имя типа, а не имя параметра.

double sum (const double a [ ] , int=25); / / присвоить типу int?

Неужели здесь действительно присваивается 25 типу int? Конечно, нет. Это вовсе не присваивание, а просто обозначение, цель которого — сообндить компи­ лятору (и программисту, сопровождаюш^ему приложение) о суш^ествовании зна­ чения по умолчанию-

Довольно типичное решение разработчиков языка C++. В нем значительно расширены возможности языка С. Язык разросся, в нем появилось много новых ключевых слов и операций. Между тем количество символов операций ограниче­ но, и уже в языке С использовались некоторые двухсимвольные операции. Так как число разумных комбинаций символов не очень велико, в C++ добавлены также ключевые слова, но необходимость изучения большого числа ключевых

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

291

слов создает впечатление перегруженности языка. C + + является

надстройкой

языка С и пытается сохранить репутацию компактного языка, простого в изучении и применении. Именно поэтому в C++ новые ключевые слова добавлены лишь для необходимости (хорошие примеры: new,delete, class, public, private и protected). По этой же причине C++ допускает повторное использование опе­ раций и ключевых слов для других целей. Мы уже видели, что операцию получения адреса & можно применять как операцию ссылки. В прототипе функции sum() C++ использует для новой цели операцию присваивания — с ее помощью зада­ ется значение параметра по умолчанию.

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

C++ допускает применение значений по умолчанию только для самых правых параметров. В середине списка параметров значения по умолчанию указывать нельзя.

i nt foo(int a=0,int b=2,double d1,double d=1.2); / / нельзя

Здесь либо нужно убрать левые значения по умолчанию (для обоих параметров int), либо дать значение по умолчанию первому параметру double.

Это нельзя считать серьезным ограничением. Ведь в любом случае значение по умолчанию можно явно переопределить.

Использование операции присваивания как операции для значения по умолча­ нию может привести к проблемам, если его спутать с обычной операцией присваи­ вания. Рассмотрим, например, функцию, динамически создающую новый узел (типа Node), инициализирующую его поле (типа Item) и ссылку на следующий узел в связанной структуре (типа Node*):

Node* createNode(Item item, Node*

next)

 

{ Node *p = new Node;

/ /

распределение памяти в динамической области

p->item = item; p->next

= next;

/ /

инициализация полей узла

return р; }

 

/ /

указатель для использования в клиенте

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

tail->next=createNode(item,0);

/ /

добавление узла к концу списка

t a i l = tail->next;

/ /

указывает на новый последний узел

Возлагать на клиента обязанности спецификации нулевого значения при каждом использовании функции createNodeO как сервера неправильно. Эти обязанности следует возложить на сервер. Тогда программный код клиента будет выглядеть так:

tail->next=createNode(item);

/ /

добавление узла к концу списка

t a i l = tail->next;

/ /

указывает на новый последний узел

Одно из возможных решений данной проблемы — применение значения по умолчанию:

Node* createNode(Item item, Node* next=0); / / прототип

Между тем, пропуск имен параметров в прототипе неожиданно создает новую проблему:

Node* createNode(Item, Node*=0);

/ / что это означает?

I 292

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

Это синтаксическая ошибка. Компилятор возмуш,ается, так как думает, что здесь используется операция * = . Хотя он не прав, оправдаться трудно. Единственный способ умилостивить его — добавить пробел между звездочкой и символом ра­ венства.

Node* createNocle(Item, Node* =0);

/ / это лучше

Вы помните о том, что, подобно языку С, язык C++ равнолушен к пробелам?

Да, в С это действительно так, а в С+Н

за некоторыми исключениями. Исклю­

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

оправдано. Это типично, например, для программирования в Windows. Неразборчивое использование значений по умолчанию затрудняет понимание

исходного кода клиента, и его следует избегать.

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

Рассмотрим простую функцию registerEventO, применяемую в системе реаль­ ного времени:

inline void

registerEventO

/ /

увеличить

счетчик событий,

{ count++;

span = 2 0 ; }

 

 

/ /

установить

интервал времени

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

registerEventO;

/ / вызов сервера в клиентском коде

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

Один из способов справиться с такого рода проблемой состоит в написании новой функции, например regEvent():

inline void

regEvent(int duration)

/ /

еще одна функция-сервер

{ count++;

span = duration; }

/ /

увеличение счетчика событий

Вполне жизнеспособное решение, но чередование вызовов функций register­ EventO и regEventO может только запутать. Кроме того, потребуется новое имя функции, а это всегда усложняет сопровождение. Наконец, лучше, когда анало­ гичные действия выполняются с помощью одной и той же функции. Если нужно нарисовать фигуру или установить контекст для ее изображения, то лучше, если именами соответствующих функций будут draw() и setContextO, а не draw1() и setContext1() или что-то подобное.

Таким образом, хорошо бы изменить функцию registerEventO, введя допол­ нительный параметр и модифицировав тело функции в соответствии с новыми требованиями:

inline void

registerEvent(int duration)

/ /

изменен заголовок

{ count++;

span = duration; }

/ /

тело тоже изменено

Глава 7 • Программирование с использованием функций C+'f

293

Теперь надо поменять вызов функции registerEventO на 10 страницах кода

с разными значениями фактического аргумента:

 

 

registerEvent(50);

registerEvent(20);

/ / новый клиентский

код

Кроме того, придется менять вызовы registerEventO на 400 страницах исходно­

го кода.

 

 

 

 

registerEvent(20);

/ /

модифицированный вызов

 

 

/ /

функции сервера

в клиентском коде

 

Данное решение требует:

1. Добавления нового клиентского кода

2. Изменения заголовка существующей функции-сервера

3 . Изменения тела существующей функции-сервера

4. Модификации имеющегося клиентского кода

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

inline void

registerEvent(int duration)

/ /

изменение заголовка

{ count++;

span = duration; }

/ /

и тела функции тоже

Прототип функции в новом и существующем клиентском коде может выглядеть так:

inline void registerEvent(int duration=20);

// прототип

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

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

Перегрузка имен функций

Перегрузка (переопределение) имен функций — еще одно усовершенствова­ ние, способствующее модульному программированию на C+-I-.

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

Вязыке С для функций нет вложенных областей действия (функция в функции)

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

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

висходном файле приведут к ошибке при компоновке. В языке С не принимаются

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

I

294 I

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

 

 

В C++ каждый класс имеет собственную, отдельную область действия. Сле­

 

 

довательно, одно и то же имя может использоваться для функции-члена и для

 

 

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

 

 

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

 

 

ни в разных областях действия не требует никаких различий в числе и типах па­

 

 

раметров. Они могут быть одинаковыми или разными — это не важно. Если две

 

 

функции определяются в разных областях действия (глобальной и области дейст­

 

 

вия класса или в двух разных областях класса), то конфликта имен не будет.

 

 

Это действительно важное улучшение в технологии разработки ПО. Требова­

 

 

ние языка С состоит в том, чтобы все имена функций были уникальными, а это

 

 

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

 

 

Быстрое увеличение количества имен затрудняет управление проектом. В крупных

 

 

проектах координация между командами программистов, работаюш,их над раз­

 

 

ными частями проекта, значительно усложняет дело. Введение классов в C+ +

 

 

устраняет большую часть подобных проблем. Но не все.

 

 

В C++ применимы те же правила области действия, что и в языке С. Вводи­

 

 

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

 

 

где они определены (это область действия класса или файла для имен типа и имен

 

 

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

 

 

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

 

 

а не только в разных.

 

 

 

 

 

C++ предлагает euie одно значительное усовершенствование в данной облас­

 

 

ти — он допускает перегрузку имен функций. Смысл имени функции в C++ зави­

 

 

сит от числа ее параметров и их типов. Использование одного имени для разных

 

 

функций с разным числом параметров называется перегрузкой имен (overloading).

 

 

Компилятор различает такие перегруженные функции.

 

 

Приведем пример применения одного имени функции addO для двух разных

 

 

функций. У них различается число параметров. Одна функция имеет два парамет­

 

 

ра, другая — три:

 

 

 

 

 

 

i nt aclcl(int х,

int

у)

/ /

два

параметра

 

 

{ return X + у;

}

 

 

 

 

 

 

int aclcl(int х,

int

у, inx z)

/ /

три

параметра

 

 

{ return X + у + z;

}

 

 

 

Если список параметров для разных функций различен, компилятор C+ + интерпретирует их как разные функции и одинаковое имя его не смуш,ает. Когда такая функция вызывается клиентом, передаваемый клиентом список параметров заставляет компилятор выбрать правильное определение функции:

int а = 2, b = 3, с, d; . . .

/ / и т.

д.

 

 

 

 

с = add^(a,b);

/ /

вызов

int

add(int

х,

int

у);

d = add(a,b,c);

/ /

вызов int

add(int

x,

int

y, inx z);

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

void

add(int *х; int у)

/ / также два параметра

{ *х

+= у; }

 

Данная функция add() также имеет два параметра, но у первого параметра другой тип — "указатель на int", а не int. Этого достаточно, чтобы компилятор различал вызовы двух функций:

int а = 2, b = 3, с, d; .. .

/ / и т. д.

с = add(a,b);

// вызов int add(int х, intу);

d = add(a,b,c);

// вызов int add(int x, int y, inxz);

add(&a,b);

// вызов void add(int *x; int y)

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

295

Как видно, в C++ смысл вызова функции зависит от ее контекста — типов фактических аргументов, подставляемых в клиенте. Для разрешения неоднознач­ ности компилятор C++ использует сигнатуру функции. Сигнатура — это просто общедоступный интерфейс функции. Он основывается на числе и типе ее аргумен­ тов. Разного порядка типов параметров достаточно, чтобы функции различались.

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

double add(int

х, int у)

/ /

сигнатура та же: синтаксическая

ошибка

{ double а = (double)x,

b = (double)y;

 

return a + b;

}

/ /

другой возвращаемый тип: этого

недостаточно

Компилятор C++

не может отличить эту функцию от первой функции add(),

возвращающей int:

 

 

 

int а = 2, b = 3, с,

d; double е; . . .

/ /

и т. д.

с = add(a,b);

 

/ /

неоднозначность: какая функция?

е = add(a,b);

 

/ /

неоднозначность: какая функция?

Здесь первый вызов используется для присваивания значения целочисленной переменной, а второй — для присваивания переменной типа double. Читателю этого достаточно, чтобы понять, какая именно функция add() вызывается, но не компилятору.

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

int add(int х, int у); double add(int х, int у);

/ /

допустимый прототип

/ /

переопределение функции:

/ /

синтаксическая ошибка

Обратите внимание, что если возвращаемые функцией типы совпадают, ком­ пилятор принимает второй прототип за простое повторное объявление функции:

int

add(int

х,

int

у);

/ /

допустимый прототип

int

add(int

х,

int

у);

/ /

повторное объявление функции: нет проблем

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

int add(int а, int b,

int с, int d)

/ /

еще одна совмещенная функция add()

{ int X = a>b?a:b,

у = c>d?c:d;

/ /

плохое применение оператора условия

return х>у ? X :

у;

}

/ /

возвращает максимальное значение

Для компилятора C++ это вполне законно. Он будет отличать данную функцию от других функций add(), руководствуясь их интерфейсами. Что касается вашего начальника (и сопровождающего приложение программиста), то нетрудно дога­ даться об их мнении на этот счет.

Перегрузка имен устраняет необходимость придумывать уникальные имена для разных, но родственных функций:

int

addPair (int,

i n t ) ;

int

addThree(int,int,int);

void

addTwo(int *,

i n t ) ;

/ /

вместо

int add(int x, int y);

/ /

вместо

int

add(int, i n t , i n t ) ;

/ /

вместо

void

add(int*, i n t ) ;

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

296

Часть li • Объектно-ориентированное про

а С++

Если компилятор не может сопоставить фактические аргументы ни одному набору формальных параметров для функции с указанным именем, он выводит сообидение о синтаксической ошибке. Если невозможно точное соответствие, то компилятор использует преобразование типов. В данном примере предполагается, что тип Item — это структура, совместимая с типом int:

i nt с; Item х; . . .

/ /

и т. д.

с = ас1с1(5,х);

/ /

нет соответствия: синтаксическая ошибка

с

= ас1с1(5, ' а ' ) ;

/ /

нет

ошибки: приведение типа

с

= ас1с1(5,20.0);

/ /

нет

ошибки: преобразование

В применении символа там, где ожидается целое значение, смысла немного. Следовало бы запретить такие действия, но это не сделано, поэтому старайтесь не применять их, если на то нет весомых причин (трудно найти реальные причины, но... никогда не говори "никогда").

 

Внимание Если для класса определены операции или конструкторы

^ Г

преобразования, для аргументов-классов компилятор C++ применяет

преобразования, заданные программистом (ниже об этом будет рассказано

подробнее).

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

long max(long х, long

у)

/ /

возвращает максимальное значение

{ return х>у ? X : у;

}

 

 

double max(double

х,

double у)

/ /

отличается от long

{ return х>у ? X :

у;

}

 

 

Когда типы аргументов в точности совпадают с формальными параметрами, компилятору C++ не стоит труда найти в клиенте верную функцию:

long а=2, Ь=3.

с;

 

 

double х=2.0,

у=3.0, z;

 

 

с = тах(а,Ь)

/ /

нет неоднозначности вызова: long max(long,

long);

Z =

тах(х,у)

/ /

нет неоднозначности вызова: double max(double,

double);

Z =

тах(а,у)

/ /

неоднозначность: какая функция

 

Здесь в последнем вызове функции первый фактический аргумент имеет тип long, а второй — double. Хотя возвраш.ается значение типа double, компилятор отказывается различать, какая функция вызывается. Это можно указать явно, приведя аргумент к соответствуюидему типу:

Z = max((double)x,y);

/ /

нет неоднозначности

 

/ /

вызова: double max(double, double);

В следующем примере делается попытка передать аргумент типа int. Очевид­ но, преобразование из int в long более естественно, чем из int в double, не так ли? Нет, не так. Возможно, это естественно для человека, но не для компилятора C++. В C++ нет такого понятия как сходство типов. Преобразование есть преобразование.

int к=2, т=3, п;

п = max(k,m);

// неоднозначность: какая функция? long? double?

Глава 7 • Программирование с использованием функций €+-*•

297

Компилятору все равно, преобразовывать int в long или int в double, а потому он помечает такой вызов как неоднозначный.

Применение такого превосходного средства, как перегрузка имен функций — само по себе достаточно сложно. Возможно, применение двух функций — maxLong() и maxDoubleO — не такая уж плохая идея. Особенно, если это еще не конец. Рассмотрим еще две перегруженные функции.

i nt min

(int х, int у)

 

/ /

возвращает минимальное значение

{ return

х>у ? X : у;

}

 

 

double min(double

х, double у)

/ /

отличается от int

{ return

х>у ? X :

у;

}

 

 

Давайте сыграем в ту же игру под названием "неоднозначность". Ответ вы знаете — компилятору все равно, преобразовывать int в long или int в double. Следовательно, такой вызов даст синтаксическую ошибку:

long к=2, т=3,

п;

п = (nin(k,m);

/ / неоднозначность: какая функция? int? double?

Рассмотрим то же самое для фактических аргументов типа short и float. Можно было бы ожидать от компилятора той же реакции, однако он транслирует этот исходный код без возражений:

long а=2, b=3

с;

 

 

float х=2Of,

У=3 Of,

 

 

с = min(a b);

/ /

нет неоднозначности

вызова: int max(int, i n t ) ;

Z = min(x У);

/ /

нет неоднозначности

вызова:

 

/ /

double max(double,

double);

Причина в том, что компилятор в этом случае не выполняет преобразования типов. Значения типа short не преобразуются в int, а просто приводятся к типу большего размера (promotion). Аналогично значения типа float приводятся к double. После такой операции компилятор может сопоставить типы аргументов и находит точное соответствие. Никакой неоднозначности нет.

Когда аргументы передаются по значению, спецификатор const считается из­ лишним. Следовательно, перегруженные функции в этом случае различить невоз­ можно. Например, такая функция не отличается от функции int min (int, int):

int min

(const int

x,

const int y)

/ / возвращает минимальное значение

{ return

x>y ? X :

у;

}

 

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

int min

(int &x, int

&y)

/ / возвращает минимальное значение

{ return

x>y ? X : у;

}

 

Но компилятор без труда различает указатели и указываемые типы, например отличает int* от int, а также указатели-константы и не константы от ссылок. В качестве иллюстрации рассмотрим две небольшие и достаточно бессмысленные функции:

void printChar

(char

ch)

/ /

параметр-значение

{ cout «

ch; }

 

 

 

 

void printChar

(char*

ch)

/ /

параметр-указатель

{ cout «

*ch;

}

 

 

 

298

Часть И « Объектно-ориентированное ороп:

рование на C+-f

Вызовы функции в клиенте выглядят различно: компилятор и программист могут видеть, когда в первом из двух вызовов функции используется обычный символ (не константа) :

char с = 'А' ; const

char со = 'А' ;

 

printChar(G);

//OK,

void

printChar(char);

printChar(&c);

/ /

OK,

void

printChar(char*);

printChar(cc);

/ /

константу

можно передавать

 

/ /

функции void printChar(char);

printChar(&cc);

/ /

константу

нельзя передавать

 

/ /

функции void printChar(char*);

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

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

void printChar

(const char* ch)

/ / указатель, но со значением-константой

{ cout « *ch;

}

 

Не все перечисленные выше вызовы функций будут компилироваться и выпол­ няться корректно. Обратите внимание, что если убрать вторую функцию (void printChar(char*);) второй вызов все равно компилируется. Он будет вызывать void printChar(const char*);. Весьма уместно (и безопасно)для передачи отлич­ ного от константы значения там, где ожидается значение-константа.

Заметьте также, что все литеральные строки, заключенные в двойные кавычки, имеют тип char*, а не const char*. Вот почему можно установить на них обычные указатели и изменять их через данные указатели:

char *р = "day"; р[0] = ' р'; / / теперь здесь "pay"

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

Хотя приведенные примеры перегрузки имен функций достаточно примитивны, они позволяют продемонстрировать, что данный механизм может затруднить пони­ мание исходного кода клиента. Иногда с^южно разобраться, какая именно функция вызывается. Это может представлять трудности и для компилятора C+ + , и для программиста, сопровождаюш^его приложение. В C++ слишком многое происхо­ дит "за кулисами", так что данное средство не стоит применять часто.

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

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

299

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

i n l i ne void

registerEventO

 

 

 

{ count++;

span = 20; }

/ /

увеличить счетчик

событий

 

 

/ / и

задать интервал

времени

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

registerEventO; / / вызов функции-сервера в клиенте

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

Конечно, всегда есть альтернативное решение — написать функцию, напри­ мер regEventO, обслуживаюшую эти 10 страниц кода:

inline void

regEvent(int duration)

/ /

другая функция-сервер

{ count++;

span = duration; }

/ /

увеличить счетчик событий

Втаком небольшом примере нетрудно написать данную маленькую функцию.

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

inline void

registerEvent(int duration)

/ /

изменяем заголовок

{ count++;

span = duration; }

/ /

и тело тоже

Как уже говорилось в предыдундем разделе, решение требует: 1. Добавления нового клиентского кода (10 страниц)

2. Изменения заголовка существуюш^ей функции-сервера (добавления нового параметра)

3 . Изменения тела суш,ествуюш,ей функции-сервера (использования нового параметра)

4. Модификации имеюидегося клиентского кода (все 400 страниц)

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

inline void

registerEvent(int duration)

/ /

новый

заголовок функции

{ count++;

span = duration; }

/ /

новое

тело функции

Таким образом, изменения сводятся к:

1. Добавлению нового клиентского кода (10 страниц)

2. Добавлению новой функции-сервера

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

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