Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
334 7.3.5. Значения параметров по умолчанию Значение параметра по умолчанию – это значение, которое разработчик считает подходящим в большинстве случаев употребления функции, хотя и не во всех. Оно освобождает программиста от необходимости уделять внимание каждой детали интерфейса функции. Значения по умолчанию для одного или нескольких параметров функции задаются с помощью того же синтаксиса, который употребляется при инициализации переменных. Например, функция для создания и инициализации двумерного массива, моделирующего экран терминала, может использовать такие значения для высоты, ширины и символа фона экрана: char background = ' ' ); Функция, для которой задано значение параметра по умолчанию, может вызываться по- разному. Если аргумент опущен, используется значение по умолчанию, в противном случае – значение переданного аргумента. Все следующие вызовы screenInit() корректны: cursor = screenlnit(66, 256, '#'); Фактические аргументы сопоставляются с формальными параметрами позиционно (в порядке следования), и значения по умолчанию могут использоваться только для подстановки вместо отсутствующих последних аргументов. В нашем примере невозможно задать значение для background, не задавая его для height и width. cursor = screenInit( , ,'?'); При разработке функции с параметрами по умолчанию придется позаботиться об их расположении. Те, для которых значения по умолчанию вряд ли будут употребляться, необходимо поместить в начало списка. Функция screenInit() предполагает (возможно, основываясь на опыте применения), что параметр height будет востребован пользователем наиболее часто. char *screenInit( int height = 24, int width = 80, char *cursor; // эквивалентно screenInit(24,80,' ') cursor = screenInit(); // эквивалентно screenInit(66,80,' ') cursor = screenlnit(66); // эквивалентно screenInit(66,256,' ') cursor = screenlnit(66, 256); // эквивалентно screenInit('?',80,' ') cursor = screenInit('?'); // ошибка, неэквивалентно screenInit(24,80,'?') С++ для начинающих 335 Значения по умолчанию могут задаваться для всех параметров или только для некоторых. При этом параметры без таких значений должны идти раньше тех, для которых они указаны. char background = ' ' ); Значение по умолчанию может указываться только один раз в файле. Следующая запись ошибочна: int ff( int i = 0) { ... } // ошибка По соглашению значение задается в объявлении функции, которое размещается в общедоступном заголовочном файле (описывающем интерфейс), а не в ее определении. Если же указать его в определении, это значение будет доступно только для вызовов функции внутри исходного файла, содержащего это определение. Можно объявить функцию повторно и таким образом задать дополнительные параметры по умолчанию. Это удобно при настройке универсальной функции для конкретного приложения. Скажем, в системной библиотеке UNIX есть функция chmod(), изменяющая режим доступа к файлу. Ее объявление содержится в системном заголовочном файле : int chmod( char *filePath, int protMode ); protMode представляет собой режим доступа, а filePath – имя и каталог файла. Если в некотором приложении файл только читается, можно переобъявить функцию chmod(), задав для соответствующего параметра значение по умолчанию, чтобы не указывать его при каждом вызове: int chmod( char *filePath, int protMode=0444 ); Если функция объявлена в заголовочном файле так: file int ff( int a, int b, int с = 0 ); // ff.h то как переобъявить ее, чтобы присвоить значение по умолчанию для параметра b? Следующая строка ошибочна, поскольку она повторно задает значение для с: // ошибка: width должна иметь значение по умолчанию, // если такое значение имеет height char *screenlnit( int height = 24, int width, // tf.h int ff( int = 0 ); // ft. С #include "ff.h" #include С++ для начинающих 336 int ff( int a, int b = 0, int с = 0 ); // ошибка Так выглядит правильное объявление: int ff( int a, int b = 0, int с ); // правильно В том месте, где мы переобъявляем функцию ff(), параметр b расположен правее других, не имеющих значения по умолчанию. Поэтому требование присваивать такие значения справа налево не нарушается. Теперь мы можем переобъявить ff() еще раз: int ff( int a = 0, int b, int с ); // правильно Значение по умолчанию не обязано быть константным выражением, можно использовать любое: int с = cDefault() ); Если такое значение является выражением, то оно вычисляется во время вызова функции. В примере выше cDefault() работает каждый раз, когда происходит вызов функции ff() без указания третьего аргумента. 7.3.6. Многоточие Иногда нельзя перечислить типы и количество всех возможных аргументов функции. В этих случаях список параметров представляется многоточием (...), которое отключает механизм проверки типов. Наличие многоточия говорит компилятору, что у функции может быть произвольное количество аргументов неизвестных заранее типов. Многоточие употребляется в двух форматах: void foo( ... ); #include "ff.h" #include "ff.h" #include "ff.h" int ff( int a, int b = 0, int с ); // правильно int aDefault(); int bDefault( int ); int cDefault( double = 7.8 ); int glob; int ff( int a = aDefault() , int b = bDefau1t( glob ) , void foo( parm_list, ... ); С++ для начинающих 337 Первый формат предоставляет объявления для части параметров. В этом случае проверка типов для объявленных параметров производится, а для оставшихся фактических аргументов – нет. Запятая после объявления известных параметров необязательна. Примером вынужденного использования многоточия служит функция printf() стандартной библиотеки С. Ее первый параметр является C-строкой: int printf( const char* ... ); Это гарантирует, что при любом вызове printf() ей будет передан первый аргумент типа const char*. Содержание такой строки, называемой форматной, определяет, необходимы ли дополнительные аргументы при вызове. При наличии в строке формата метасимволов, начинающихся с символа %, функция ждет присутствия этих аргументов. Например, вызов printf( "hello, world\n" ); имеет один строковый аргумент. Но printf( "hello, %s\n", userName ); имеет два аргумента. Символ % говорит о наличии второго аргумента, а буква s, следующая за ним, определяет его тип – в данном случае символьную строку. Большинство функций с многоточием в объявлении получают информацию о типах и количестве фактических параметров по значению явно объявленного параметра. Следовательно, первый формат многоточия употребляется чаще. Отметим, что следующие объявления неэквивалентны: void f( ... ); В первом случае f() объявлена как функция без параметров, во втором – как имеющая ноль или более параметров. Вызовы f( cnt, a, b, с ); корректны только для второго объявления. Вызов f(); применим к любой из двух функций. Упражнение 7.4 Какие из следующих объявлений содержат ошибки? Объясните. void f(); f( someValue ); С++ для начинающих 338 (c) void operate( int *matrix[] ); (e) void putValues( int (&ia)[] ); Упражнение 7.5 Повторные объявления всех приведенных ниже функций содержат ошибки. Найдите их. void manip( int *pi, int first = 0, int end = 0 ); Упражнение 7.6 Даны объявления функций. char background = ' ' ); Вызовы этих функций содержат ошибки. Найдите их и объясните. print( arr, 5 ); Упражнение 7.7 Перепишите функцию putValues( vector ( 2 ) (a) void print( int arr[][], int size ); (b) int ff( int a, int b = 0, int с = 0 ); (d) char *screenInit( int height = 24, int width, char background ); (a) char *screenInit( int height, int width, char background = ' ' ); char *screenInit( int height = 24, int width, char background ); (b) void print( int (*arr)[6], int size ); void print( int (*arr)[5], int size ); (c) void manip( int *pi, int first, int end = 0 ); void print( int arr[][5], int size ); void operate(int *matrix[7]); char *screenInit( int height = 24, int width = 80, (a) screenInit(); (b) int *matrix[5]; operate( matrix ); (c) int arr[5][5]; С++ для начинающих 339 < "first string" "second string" > Напишите функцию main(), вызывающую новый вариант putValues() со следующим списком строк: "use less than eight parameters" Упражнение 7.8 В каком случае вы применили бы параметр-указатель? А в каком – параметр-ссылку? Опишите достоинства и недостатки каждого способа. 7.4. Возврат значения В теле функции может встретиться инструкция return. Она завершает выполнение функции. После этого управление возвращается той функции, из которой была вызвана данная. Инструкция return может употребляться в двух формах: return expression; Первая форма используется в функциях, для которых типом возвращаемого значения является void. Использовать return в таких случаях обязательно, если нужно принудительно завершить работу. (Такое применение return напоминает инструкцию break , представленную в разделе 5.8.) После конечной инструкции функции подразумевается наличие return. Например: "put function declarations in header files" "use abstract container types instead of built-in arrays" "declare class parameters as references" "use reference to const types for invariant parameters" return; С++ для начинающих 340 } Во второй форме инструкции return указывается то значение, которое функция должна вернуть. Это значение может быть сколь угодно сложным выражением, даже содержать вызов функции. В реализации функции factorial(), которую мы рассмотрим в следующем разделе, используется return следующего вида: return val * factorial(val-1); В функции, не объявленная с void в качестве типа возвращаемого значения, обязательно использовать вторую форму return, иначе произойдет ошибка компиляции. Хотя компилятор не отвечает за правильность результата, он сможет гарантировать его наличие. Следующая программа не компилируется из-за двух мест, где программа завершается без возврата значения: void d_copy( double "src, double *dst, int sz ) { /* копируем массив "src" в "dst" * для простоты предполагаем, что они одного размера */ // завершение, если хотя бы один из указателей равен 0 if ( !src || !dst ) return; // завершение, // если указатели адресуют один и тот же массив if ( src == dst ) return; // копировать нечего if ( sz == 0 ) return; // все еще не закончили? // тогда самое время что-то сделать for ( int ix = 0; ix < sz; ++ix ) dst[ix] = src[ix]; // явного завершения не требуется С++ для начинающих 341 } Если тип возвращаемого значения не точно соответствует указанному в объявлении функции, то применяется неявное преобразование типов. Если же стандартное приведение невозможно, происходит ошибка компиляции. (Преобразования типов рассматривались в разделе 4.1.4.) По умолчанию возвращаемое значение передается по значению, т.е. вызывающая функция получает копию результата вычисления выражения, указанного в инструкции return . Например: } grow() возвращает вызывающей функции копию значения, хранящегося в переменной val Такое поведение можно изменить, если объявить, что возвращается указатель или ссылка. При возврате ссылки вызывающая функция получает l-значение для val и потому может модифицировать val или взять ее адрес. Вот как можно объявить, что grow() возвращает ссылку: // определение интерфейса класса Matrix #include "Matrix.h" bool is_equa1( const Matrix &ml, const Matrix &m2 ) { /* Если содержимое двух объектов Matrix одинаково, * возвращаем true; * в противном случае - false */ // сравним количество столбцов if ( ml.colSize() != m2.co1Size() ) // ошибка: нет возвращаемого значения return; // сравним количество строк if ( ml.rowSize() != m2.rowSize() ) // ошибка: нет возвращаемого значения return; // пробежимся по обеим матрицам, пока // не найдем неравные элементы for ( int row = 0; row < ml.rowSize(); ++row ) for ( int col = 0; co1 < ml.colSize(); ++co1 ) if ( ml[row][col] != m2[row][col] ) return false; // ошибка: нет возвращаемого значения // для случая равенства Matrix grow( Matrix* p ) { Matrix val; // ... return val; С++ для начинающих 342 } Если возвращается большой объект, то гораздо эффективнее перейти от возврата по значению к использованию ссылки или указателя. В некоторых случаях компилятор может сделать это автоматически. Такая оптимизация получила название именованное возвращаемое значение. (Она описывается в разделе 14.8.) Объявляя функцию как возвращающую ссылку, программист должен помнить о двух возможных ошибках: • возврат ссылки на локальный объект, время жизни которого ограничено временем выполнения функции. (О времени жизни локальных объектов речь пойдет в разделе 8.3.) По завершении функции такой ссылке соответствует область памяти, содержащая неопределенное значение. Например: } В таком случае тип возврата не должен быть ссылкой. Тогда локальная переменная может быть скопирована до окончания времени своей жизни: Matrix add( ... ) • функция возвращает l-значение. Любая его модификация затрагивает сам объект. Например: Matrix& grow( Matrix* p ) { Matrix *res; // выделим память для объекта Matrix // большого размера // res адресует этот новый объект // скопируем содержимое *p в *res return *res; // ошибка: возврат ссылки на локальный объект Matrix& add( Matrix &m1, Matrix &m2 ) { Matrix result: if ( m1.isZero() ) return m2; if ( m2.isZero() ) return m1; // сложим содержимое двух матриц // ошибка: ссылка на сомнительную область памяти // после возврата return result; С++ для начинающих 343 } Для предотвращения нечаянной модификации возвращенного объекта нужно объявить тип возврата как const: const int &get_val( ... ) Примером ситуации, когда l-значение возвращается намеренно, чтобы позволить модифицировать реальный объект, может служить перегруженный оператор взятия индекса для класса IntArray из раздела 2.3. 7.4.1. Передача данных через параметры и через глобальные объекты Различные функции программы могут общаться между собой с помощью двух механизмов. (Под словом “общаться” мы подразумеваем обмен данными.) В одном случае используются глобальные объекты, в другом – передача параметров и возврат значений. Глобальный объект определен вне функции. Например: } Объект glob является глобальным. (В главе 8 рассмотрение глобальных объектов и глобальной области видимости будет продолжено.) Главное достоинство и одновременно один из наиболее заметных недостатков такого объекта – доступность из любого места программы, поэтому его обычно используют для общения между разными модулями. Обратная сторона медали такова: • функции, использующие глобальные объекты, зависят от этих объектов и их типов. Использовать такую функцию в другом контексте затруднительно; • при модификации такой программы повышается вероятность ошибок. Даже для внесения локальных изменений необходимо понимание всей программы в целом; #include } int ai[4] = { 0, 1, 2, 3 }; vector // увеличивает vec[0] на 1 get_val( vec.0 )++; // ... int glob; int main() { // что угодно С++ для начинающих 344 • если глобальный объект получает неверное значение, ошибку нужно искать по всей программе. Отсутствует локализация; • используя глобальные объекты, труднее писать рекурсивные функции (Рекурсия возникает тогда, когда функция вызывает сама себя. Мы рассмотрим это в разделе 7.5.); • если используются потоки (threads), то для синхронизации доступа к глобальным объектам требуется писать дополнительный код. Отсутствие синхронизации – одна из распространенных ошибок при использовании потоков. (Пример использования потоков при программировании на С++ см. в статье “Distributing Object Computing in C++” (Steve Vinoski and Doug Schmidt) в [LIPPMAN96b].) Можно сделать вывод, что для передачи информации между функциями предпочтительнее пользоваться параметрами и возвращаемыми значениями. Вероятность ошибок при таком подходе возрастает с увеличением списка. Считается, что восемь параметров – это приемлемый максимум. В качестве альтернативы длинному списку можно использовать в качестве параметра класс, массив или контейнер. Он способен содержать группу значений. Аналогично программа может возвращать только одно значение. Если же логика требует нескольких, некоторые параметры объявляются ссылками, чтобы функция могла непосредственно модифицировать значения соответствующих фактических аргументов и использовать эти параметры для возврата дополнительных значений, либо некоторый класс или контейнер, содержащий группу значений, объявляется типом, возвращаемым функцией. Упражнение 7.9 Каковы две формы инструкции return? Объясните, в каких случаях следует использовать первую, а в каких вторую форму. Упражнение 7.10 Найдите в данной функции потенциальную ошибку времени выполнения: } Упражнение 7.11 Каким способом вы вернули бы из функции несколько значений? Опишите достоинства и недостатки вашего подхода. vector // ... } // .... return text; С++ для начинающих 345 7.5. Рекурсия Функция, которая прямо или косвенно вызывает сама себя, называется рекурсивной. Например: } Такая функция обязательно должна определять условие окончания, в противном случае рекурсия будет продолжаться бесконечно. Подобную ошибку так иногда и называют – бесконечная рекурсия. Для rgcd() условием окончания является равенство нулю остатка. Вызов rgcd( 15, 123 ); возвращает 3 (см. табл. 7.1). |