
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf290 |
Часть 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+'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-.
Вбольшинстве языков каждое имя связывается с уникальным объектом в об ласти действия (блоке, функции, классе, файле или программе). Это касается имен типов, переменных и функций.
Вязыке С для функций нет вложенных областей действия (функция в функции)
иих имена должны быть уникальны в области действия программы, а не только
вобласти действия файла. Два определения функции с одним и тем же именем
висходном файле приведут к ошибке при компоновке. В языке С не принимаются
врасчет типы параметров или возвращаемых значений. Важно только имя функ ции, и оно должно быть уникально в проекте (включая библиотеки).
Глава 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. Добавлению новой функции-сервера
Здесь исключаются не только изменения в суш,ествуюш,ем коде, но и изменения в имеюш,ейся функции-сервере. Замечательно! Не каждая задача поддается тако му методу, но, если это так, не упускайте возможности. Это одно из наиболее серьезных усовершенствований в традиционной технологии сопровождения ПО.