Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
ErrCode, каков тип элемента в цикле for? Упражнение 6.29. При использовании типа initializer_list в серийном операторе for использовали бы вы ссылку как управляющую переменную цикла? Объясните почему. 6.3. Типы возвращаемого значения и оператор return Оператор return завершает выполнение функции и возвращает управление той функции, которая вызвала текущую. Существуют две формы оператора return: return; return выражение ; 6.3.1. Функции без возвращаемого значения Оператор return без значения применим только в такой функции, типом возвращаемого значения которой объявлен void. Функции, возвращаемым типом которых объявлен void, необязательно должны содержать оператор return. В функции типа void оператор return неявно размещается после последнего оператора. Как правило, функции типа void используют оператор return для преждевременного завершения выполнения. Это аналогично использованию оператора break (см. раздел 5.5.1) для выход из цикла. Например, можно написать функцию swap(), которая не делает ничего, если значения идентичны: void swap(int &v1, int &v2) { // если значения равны, их замена не нужна; можно выйти сразу Page 287/1103 if (v1 == v2) return; // если мы здесь, придется поработать int tmp = v2; v2 = v1; v1 = tmp; // явно указывать оператор return не обязательно } Сначала эта функция проверяет, не равны ли значения, и если это так, то завершает работу. Если значения не равны, функция меняет их местами. После последнего оператора присвоения осуществляется неявный выход из функции. Функции, для возвращаемого значения которых указан тип void, вторую форму оператора return могут использовать только для возвращения результата вызова другой функции, которая возвращает тип void. Возвращение любого другого выражения из функции типа void приведет к ошибке при компиляции. 6.3.2. Функции, возвращающие значение Вторая форма оператора return предназначена для возвращения результата из функции. Каждый случай возвращения значения типа, отличного от void, должен возвратить значение. Возвращаемое значение должно иметь тип, либо совпадающий, либо допускающий неявное преобразование (см. раздел 4.11) в тип, указанный для возвращаемого значения функции при определении. Хотя язык С++ не может гарантировать правильность результата, он способен гарантировать, что каждое возвращаемое функцией значение будет соответствовать объявленному типу. Это может получиться не во всех случаях, компилятор попытается обеспечить возвращение значения и выход только через допустимый оператор return. Например: // некорректное возвращение значения, этот код не будет откомпилирован bool str_subrange(const string &str1, const string &str2) { // размеры одинаковы: возвратить обычный результат сравнения if (str1.size() == str2.size()) Page 288/1103 return str1 == str2; // ok: == возвращает bool // найти размер меньшей строки; условный оператор см. раздел 4.7 auto size = (str1.size() < str2.size()) ? str1.size() : str2.size(); // просмотреть все элементы до размера меньшей строки for (decltype(size) i = 0; i != size; ++i) { if (str1[i] != str2[i]) return; // ошибка #1: нет возвращаемого значения; компилятор // должен обнаружить эту ошибку } // ошибка #2: выполнение может дойти до конца функции, так и // не встретив оператор return // компилятор может и не обнаружить эту ошибку } Оператор return в цикле for является ошибочным потому, что он не в состоянии вернуть значение. Эту ошибку компилятор должен обнаружить. Вторая ошибка заключается в том, что функция не имеет оператора return после цикла. Если произойдет вызов этой функции со строкой, являющейся подмножеством другой, процесс выполнения минует цикл for. Однако оператор return для этого случая не предусмотрен. Эту ошибку компилятор может и не обнаружить. В этом случае поведение программы во время выполнения будет непредсказуемо. Отсутствие оператора return после цикла, который этот оператор содержит, является особенно коварной ошибкой. Однако большинство компиляторов ее не обнаружит. Как возвращаются значения Значения функций возвращаются тем же способом, каким инициализируются переменные и параметры: возвращаемое значение используется для инициализации временного объекта в Page 289/1103 точке вызова, и этот временный объект является результатом вызова функции. В функциях, возвращающих локальные переменные, важно иметь в виду правила инициализации. Например, можно написать функцию, которой передают счетчик, слово и окончание. Функция возвращает множественную форму слова, если счетчик больше 1: // возвратить множественную форму слова, если ctr больше 1 string make_plural(size_t ctr, const string &word, const string &ending) { return (ctr > 1) ? word + ending : word; } Тип возвращаемого значения этой функции — string, это значит, что возвращаемое значение копируется в точке вызова. Функция возвращает копию значения word или безымянную временную строку, полученную конкатенацией word и ending. Когда функция возвращает ссылку, она, подобно любой другой ссылке, является только другим именем для объекта, на который ссылается. Рассмотрим, например, функцию, возвращающую ссылку на более короткую из двух переданный ей строк: // возвратить ссылку на строку, которая короче const string &shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } Параметры и возвращаемое значение имеют тип ссылки на const string. Строки не копируются ни при вызове функции, ни при возвращении результата. Никогда не возвращайте ссылку на локальный объект По завершении работы функции все хранилища ее локальных объектов освобождаются (см. раздел 6.1.1). Поэтому после завершения работы функции ссылки на ее локальные объекты ссылаются на несуществующие объекты. // катастрофа: функция возвращает ссылку на локальный объект const string &manip() { string ret; // обработать ret некоторым образом if (!ret.empty()) return ret; // ошибка: возвращение ссылки на локальный объект! Page 290/1103 else return "Empty"; // ошибка: "Empty" - локальная временная строка } Эта функция приведет к отказу во время выполнения, поскольку она возвращает ссылку на локальный объект. Когда функция завершит работу, область памяти, которую занимала переменная ret, будет освобождена. Возвращаемое значение будет ссылаться на ту область памяти, которая уже недоступна. Оба оператора return возвращают здесь неопределенное значение — неизвестно, что будет, попробуй мы использовать значение, возвращенное функцией manip(). В первом операторе return очевидно, что функция пытается вернуть ссылку на локальный объект. Во втором случае строковый литерал преобразуется в локальный временный объект класса string. Этот объект, как и строка s, является локальным объектом функции manip(). Область памяти, в которой располагается временный объект, освобождается по завершении функции. Оба оператора return возвращают ссылки на области памяти, которые больше недоступны. Чтобы удостовериться в безопасности возвращения значения, следует задаться вопросом: к какому существовавшему ранее объекту относится ссылка? Возвращать указатель на локальный объект нельзя по тем же причинам. По завершении функции локальные объекты освобождаются, и указатель указывает на несуществующий объект. Функции, возвращающие типы класса и оператор вызова Подобно любому оператору, оператор вызова обладает порядком (ассоциативностью) и приоритетом (см. раздел 4.1.2). У оператора вызова тот же приоритет, что и у операторов "точка" и "стрелка" (см. раздел 4.6). Как и эти операторы, оператор вызова имеет левосторонний порядок. В результате, если функция возвращает указатель, ссылку или объект типа класса, результат вызова можно использовать для обращения к члену полученного объекта. Например, размер более короткой строки можно определить следующим образом: // обращение к методу size объекта строки, возвращенной shorterString auto sz = shorterString(s1, s2).size(); Поскольку эти операторы имеют левосторонний порядок, результат вызова функции shorterString() будет левым операндом точечного оператора. Этот оператор выбирает метод size() объекта строки. Этот метод становится левым операндом второго оператора вызова. Возвращаемая ссылка является l-значением Является ли вызов функции l-значением (см. раздел 4.1.1), зависит от типа возвращаемого значения функции. Вызовы функции, возвращающей ссылку, являются l-значением; другие типы возвращаемого значения являются r-значениями. Вызов функции, возвращающей ссылку, применяется таким же способом, как и любое другое l-значение. В частности, можно осуществлять присвоение результату вызова функции, возвращающей ссылку на неконстанту: Page 291/1103 char &get_val(string &str, string::size_type ix) { return str[ix]; // get_val подразумевает, что данный индекс допустим } int main() { string s("a value"); cout << s << endl; // отображает значение get_val(s, 0) = 'A'; // изменяет s[0] на A cout << s << endl; // отображает значение A return 0; } Может быть несколько странно видеть вызов функции слева от оператора присвоения. Однако в этом нет ничего необычного. Возвращаемое значение — ссылка, поэтому вызов — это l-значение, а любое l-значение может быть левым операндом оператора присвоения. Если тип возвращаемого значения является ссылкой на константу, то (как обычно) присвоение результату вызова невозможно: shorterString("hi", "bye") = "X"; // ошибка: возвращаемое значение // является константой Списочная инициализация возвращаемого значения По новому стандарту функции могут возвращать окруженный скобками список значений. Подобно любому другому случаю возвращения значения, список используется для инициализации временного объекта, представляющего возвращение функцией значения. Если список пуст, временный объект инициализируется значением по умолчанию (см. раздел 3.3.1). В противном случае возвращаемое значение зависит от типа возвращаемого значения функции. Для примера вернемся к функции error_msg из раздел 6.2.6. Эта функция получала переменное количество строковых аргументов и выводило сообщение об ошибке, составленное из переданных строк. Теперь вместо вызова функции error_msg() мы возвратим вектор, содержащий строки сообщений об ошибке: vector<string> process() { // ... Page 292/1103 // expected и actual - строки if (expected.empty()) return {}; // возвратить пустой вектор else if (expected == actual) return {"functionX", "okay"}; // возвратить вектор // инициализированный списком else return {"functionX", expected, actual}; } В первом операторе return возвращается пустой список. В данном случае возвращенный обработанный вектор будет пуст. В противном случае возвращается вектор, инициализированный двумя или тремя элементами, в зависимости от того, равны ли expected и actual. У функции, возвращающей встроенный тип, заключенный в скобки список может содержать хотя бы одно значение, и это значение не должно требовать сужающего преобразования (см. раздел 2.2.1). Если функция возвращает тип класса, то используемые инициализаторы определяет сам класс (см. раздел 3.3.1). Возвращение значения из функции main() Есть одно исключение из правила, согласно которому функция с типом возвращаемого значения, отличного от void, обязана возвратить значение: функция main() может завершить работу без возвращения значения. Если процесс выполнения достигает конца функции main() и нет никакого значения для возвращения, компилятор неявно добавляет возвращение значения 0. Как упоминалось в разделе 1.1, значение, возвращаемое из функции main(), рассматривается как индикатор состояния. Возвращение нулевого значения означает успех; большинство других значений — неудачу. У значения, отличного от нуля, есть машинно-зависимое значение. Чтобы сделать его независимым от машины, заголовок cstdlib определяет две переменные препроцессора (см. раздел 2.3.2), которые можно использовать для индикации успеха или отказа: int main() { if (some failure) return EXIT_FAILURE; // определено в cstdlib else return EXIT_SUCCESS; // определено в cstdlib Page 293/1103 } Поскольку это переменные препроцессора, им не должна предшествовать часть std:: и их нельзя использовать в объявлениях using. Рекурсия Функция, которая вызывает себя прямо или косвенно, является рекурсивной функцией (recursive function). В качестве примера можно переписать функцию вычисления факториала так, чтобы использовать рекурсию: // вычислить val!, т.е. 1 * 2 * 3 ... * val int factorial(int val) { if (val > 1) return factorial(val-1) * val; return 1; } В этой реализации осуществляется рекурсивный вызов функции factorial(), чтобы вычислить факториал числа, начиная со значения, первоначально переданного val, и далее в обратном порядке. Когда значение val достигнет 1, рекурсия останавливается и возвращается значение 1. В рекурсивной функции всегда должно быть определено условие выхода или останова (stopping condition); в противном случае рекурсия станет бесконечной, т.е. функция продолжит вызывать себя до тех пор, пока стек программы не будет исчерпан. Иногда эта ошибка называется бесконечной рекурсией (infinite recursion). В случае функции factorial() условием выхода является равенство значения параметра val единице. Ниже приведена трассировка выполнения функции factorial() при передаче ей значения 5. Трассировка вызова функции factorial(5) Вызов Возвращает Значение factorial(5) factorial(4) * 5 120 factorial(4) factorial(3) * 4 24 factorial(3) factorial(2) * 3 6 factorial(2) factorial(1) * 2 2 factorial(1) 1 1 Функция main() не может вызывать сама себя.Упражнения раздела 6.3.2 Упражнение 6.30. Откомпилируйте версию функции str_subrange(), представленной в начале раздела, и посмотрите, что ваш компилятор делает с указанными сообщениями об ошибках. Упражнение 6.31. Когда допустимо возвращение ссылки? Когда ссылки на константу? Упражнение 6.32. Укажите, корректна ли следующая функция. Если да, то объясните, что она делает; в противном случае исправьте ошибки, а затем объясните все. Page 294/1103 int &get(int *arry, int index) { return arry[index]; } int main() { int ia[10]; for (int i = 0; i != 10; ++i) get(ia, i) = i; } Упражнение 6.33. Напишите рекурсивную функцию, выводящую содержимое вектора. Упражнение 6.34. Что случится, если условие остановки функции factorial() будет таким: if (val != 0) Упражнение 6.35. Почему в вызове функции factorial() мы передали val-1, а не val--? 6.3.3. Возвращение указателя на массив Поскольку копировать массив нельзя, функция не может возвратить его. Но функция может возвратить указатель или ссылку на массив (см. раздел 3.5.1). К сожалению, синтаксис, обычно используемый для определения функций, которые возвращают указатели или ссылки на массив, довольно сложен. К счастью, такие объявления можно упростить. Например, можно использовать псевдоним типа (см. раздел 2.5.1): typedef int arrT[10]; // arrT синоним для типа массива из десяти // целых чисел using arrtT = int[10]; // эквивалентное объявление arrT; // см. раздел 2.5.1 arrT* func(int i); // func возвращает указатель на массив из // пяти целых чисел где arrT — это синоним для массива из десяти целых чисел. Поскольку нельзя возвратить массив, мы определяем тип возвращаемого значения как указатель на этот тип. Таким образом, функция func() получает один аргумент типа int и возвращает указатель на массив Page 295/1103 из десяти целых чисел. Объявление функции, возвращающей указатель на массив Чтобы объявить функцию func(), не используя псевдоним типа, следует вспомнить, что размерность массива следует за определяемым именем: int arr[10]; // arr массив из десяти целых чисел int *p1[10]; // p1 массив из десяти указателей int (*p2)[10] = &arr; // p2 указывает на массив из десяти целых чисел Подобно этим объявлениям, если необходимо определить функцию, которая возвращает указатель на массив, размерность должна следовать за именем функции. Однако функция имеет список параметров, который также следует за именем. Список параметров предшествует размерности. Следовательно, функция, которая возвращает указатель на массив, имеет такую форму: Тип (* функция ( список_параметров ))[ размерность ] Как и в любом другом объявлении массива, Тип — это тип элементов, а размерность — это размер массива. Круглые скобки вокруг части (* функция ( список_параметров )) необходимы по той же причине, по которой они были нужны при определили указателя p2. Без них мы определили бы функцию, которая возвращает массив указателей. В качестве конкретного примера рассмотрим следующее объявление функции func(), не использующей псевдоним типа: int (*func(int i))[10]; Чтобы понять это объявление, имеет смысл прочитать его следующим образом: • func(int) указывает, что функцию func() можно вызвать с аргументом типа int; • (*func(int)) указывает, что можно обратиться к значению результата этого вызова; • (*func(int))[10] указывает, что обращение к значению результата вызова функции func() возвращает массив из десяти элементов; • int (*func(int))[10] указывает, что типом элементов этого массива является int. Использование замыкающего типа возвращаемого значения Page 296/1103 По новому стандарту есть и другой способ упростить объявления функции func() — с использованием замыкающего типа возвращаемого значения (trailing return type). Оно может быть определено для любой функции, но полезней всего оно для функций со сложными типами возвращаемого значения, такими как указатели (или ссылки) на массивы. Замыкающий тип возвращаемого значения следует за списком параметров и предваряется символом ->. Чтобы сообщить о том, что возвращаемое значение следует за списком параметров, ключевое слово auto располагается там, где обычно присутствует тип возвращаемого значения: // fcn получает аргумент типа int и возвращает указатель на массив // из десяти целых чисел auto func(int i) -> int(*)[10]; Поскольку тип возвращаемого значения указан после списка параметров, проще заметить, что функция func() возвращает указатель и что этот указатель указывает на массив из десяти целых чисел. Использование спецификатора decltype В качестве другой альтернативы, если известен массив (массивы), указатель на который способна возвратить наша функция, можно использовать спецификатор decltype, чтобы объявить тип возвращаемого значения. Например, следующая функция возвращает указатель на один из двух массивов, в зависимости от значения ее параметра: int odd[] = {1,3,5,7,9}; int even[] = {0,2,4,6,8}; // возвращает указатель на массив из пяти элементов типа int decltype(odd) *arrPtr(int i) { return (i % 2) ? &odd : &even; // возвращает указатель на массив } Тип возвращаемого значения функции arrPtr() указан как decltype, свидетельствуя о том, что функция возвращает указатель на любой тип, который имеет odd. В данном случае этот объект является массивом, поэтому функция arrPtr() возвращает указатель на массив из пяти целых чисел. Единственная сложность здесь в том, что следует помнить, что спецификатор decltype не преобразовывает автоматически массив в указатель соответствующего ему типа. Тип, возвращенный спецификатором decltype, является типом массива, для которого нужно добавить *, чтобы указать, что функция arrPtr() возвращает указатель. Упражнения раздела 6.3.3 Упражнение 6.36. Напишите объявление функции, возвращающей ссылку на массив из десяти строк, не используя ни замыкающий тип возвращаемого значения, ни спецификатор decltype или псевдоним типа. Page 297/1103 Упражнение 6.37. Напишите три дополнительных объявления для функций предыдущего упражнения. Нужно использовать псевдоним типа, замыкающий тип возвращаемого значения и спецификатор decltype. Какую форму вы предпочитаете и почему? Упражнение 6.38. Перепишите функцию arrPtr() так, чтобы она возвращала ссылку на массив. 6.4. Перегруженные функции Функции, расположенные в одной области видимости, называются перегруженными (overloaded), если они имеют одинаковые имена, но разные списки параметров. Пример определения нескольких функций по имени print() приведен в разделе 6.2.4: void print(const char *cp); void print(const int *beg, const int *end); void print(const int ia[], size_t size); Эти функции выполняют одинаковое действие, но их параметры относятся к разным типам. При вызове такой функции компилятор принимает решение о применении конкретной версии на основании типа переданного аргумента: int j[2] = {0, 1}; print("Hello World"); // вызов print (const char*) print(j, end(j) - begin(j)); // вызов print(const int*, size_t) print(begin(j), end(j)); // вызов print(const int*, const int*) Перегрузка функций избавляет от необходимости придумывать (и помнить) имена, существующие только для того, чтобы помочь компилятору выяснить, которую из функций применять при вызове. Функция main() не может быть перегружена.Определение перегруженных функций Рассмотрим приложение базы данных с несколькими функциями для поиска записи на основании имени, номера телефона, номер счета и т.д. Перегрузка функций позволит определить коллекцию функций, каждая по имени lookup(), которые отличаются тем, как они осуществляют поиск. Мы сможем вызвать функцию lookup(), передав значение любого из следующих типов: Record lookup(const Account&); // Page 298/1103 поиск по счету Record lookup(const Phone&); // поиск по телефону Record lookup(const Name&); // поиск по имени Account acct; Phone phone; Record r1 = lookup(acct); // вызов версии, получающей Account Record r2 = lookup(phone); // вызов версии, получающей Phone Здесь у всех трех функций одинаковое имя, но все же это три разные функции. Чтобы выяснить, которую из них вызвать, компилятор использует тип (типы) аргументов. Перегруженные функции должны отличаться по количеству или типу (типам) своих параметров. Каждая из функций выше получает один параметр, но типы у этих параметров разные. Функции не могут отличаться только типами возвращаемого значения. Если списки параметров функций совпадают, а типы возвращаемого значения отличаются, то это будет ошибкой: Record lookup(const Account&); bool lookup(const Account&); // ошибка: отличается только типом // возвращаемого значения Различие типов параметров Два списка параметров могут быть идентичными, даже если они не выглядят одинаково: // каждая пара объявляет ту же функцию Record lookup(const Account &acct); Record lookup(const Account&); // имена параметров игнорируются typedef Phone Telno; Record lookup(const Phone&); Page 299/1103 Record lookup(const Telno&); // Telno и Phone того же типа Первое объявление в первой паре именует свой параметр. Имена параметров предназначены только для документирования. Они не изменяют список параметров. Во второй паре типы только выглядят разными, Telno — не новый тип, это только синоним типа Phone. Псевдоним типа (см. раздел 2.5.1) предоставляет альтернативное имя для уже существующего типа, а не создает новый тип. Поэтому два параметра, отличающиеся только тем, что один использует имя типа, а другой его псевдоним, не являются разными. Перегрузка и константные параметры Как упоминалось в разделе 6.2.3, спецификатор const верхнего уровня (см. раздел 2.4.3) никак не влияет на объекты, которые могут быть переданы функции. Параметр, у которого есть спецификатор const верхнего уровня, неотличим от такового без спецификатора const верхнего уровня: Record lookup(Phone); Record lookup(const Phone); // повторно объявляет Record lookup(Phone) Record lookup(Phone*); Record lookup(Phone* const); // повторно объявляет // Record lookup(Phone*) Здесь вторые объявления повторно объявляет ту же функцию, что и первые. С другой стороны, функцию можно перегрузить на основании того, является ли параметр ссылкой (или указателем) на константную или неконстантную версию того же типа; речь идет о спецификаторе const нижнего уровня: // функции, получающие константную и неконстантную ссылку (или // указатель), имеют разные параметры Record lookup(Account&); // функция получает ссылку на Account Record lookup(const Account&); // новая функция получает константную // ссылку Page 300/1103 Record lookup(Account*); // новая функция получает указатель // на Account Record lookup(const Account*); // новая функция получает указатель на // константу В этих случаях компилятор может использовать константность аргумента, чтобы различить, какую функцию применять. Поскольку нет преобразования (см. раздел 4.11.2) из константы, можно передать константный объект (или указатель на константу) только версии с константным параметром. Так как преобразование в константу возможно, можно вызвать функцию и неконстантного объекта, и указателя на неконстантный объект. Однако, как будет представлено в разделе 6.6.1, компилятор предпочтет неконстантные версии при передаче неконстантного объекта или указателя на неконстантный объект. Совет. Когда не следует перегружать функции Хотя перегрузка функций позволяет избежать необходимости создавать и запоминать имена общепринятых операций, она не всегда целесообразна. В некоторых случаях разные имена функций предоставляют дополнительную информацию, которая упрощает понимание программы. Давайте рассмотрим набор функций-членов класса Screen, отвечающих за перемещение курсора. Screen& moveHome(); Screen& moveAbs(int, int); Screen& moveRel(int, int, string direction); На первый взгляд может показаться, что этот набор функций имеет смысл перегрузить под именем move: Screen& move(); Screen& move(int, int); Screen& move(int, int, string direction); Однако при перегрузке этих функций мы потеряли информацию, которая была унаследована именами функции. Хотя перемещение курсора — это общая операция, совместно используемая всеми этими функциями, специфический характер перемещения уникален для каждой из этих функций. Рассмотрим, например, функцию moveHome(), осуществляющую вполне определенное перемещение курсора. Какое из двух приведенных ниже обращений понятнее при чтении кода? // которая из записей понятней? myScreen.moveHome(); // Page 301/1103 вероятно, эта! myScreen.move(); Оператор const_cast и перегрузка В разделе 4.11.3 упоминалось, что оператор const_cast особенно полезен в контексте перегруженных функций. В качестве примера вернемся к функции shorterString() из раздела 6.3.2: // возвратить ссылку на строку, которая короче const string &shorterString (const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } Эта функция получает и возвращает ссылки на константную строку. Мы можем вызвать функцию с двумя неконстантными строковыми аргументами, но как результат получим ссылку на константную строку. Могла бы понадобиться версия функции shorterString(), которая, получив неконстантные аргументы, возвратит обычную ссылку. Мы можем написать эту версию функции, используя оператор const_cast: string &shorterString(string &s1, string &s2) { auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&>(r); } Эта версия вызывает константную версию функции shorterString() при приведении типов ее аргументов к ссылкам на константу. Функция возвращает ссылку на тип const string, которая, как известно, привязана к одному из исходных, неконстантных аргументов. Следовательно, приведение этой строки назад к обычной ссылке string& при возвращении вполне безопасно. Вызов перегруженной функции Когда набор перегруженных функций определен, необходима возможность вызвать их с соответствующими аргументами. Подбор функции (function matching), известный также как поиск перегруженной функции (overload resolution), — это процесс, в ходе которого вызов функции ассоциируется с определенной версией из набора перегруженных функций. Компилятор определяет, какую именно версию функции использовать при вызове, сравнивая аргументы вызова с параметрами каждой функции в наборе. Как правило, вовсе несложно выяснить, допустим ли вызов, и если он допустим, то какая из версий функции будет использована компилятором. Функции в наборе перегруженных версий отличаются количеством или типом аргументов. В таких случаях определить используемую функцию просто. Подбор функции усложняется в случае, когда количество параметров одинаково и они допускают преобразование (см. раздел 4.11) переданных аргументов. Распознавание вызовов компилятором при наличии преобразований рассматривается в разделе 6.6, а пока следует понять, что при любом вызове перегруженной функции возможен один из трех результатов. Page 302/1103 • Компилятор находит одну функцию, которая является наилучшим соответствием (best match) для фактических аргументов, и создает код ее вызова. • Компилятор не может найти ни одной функции, параметры которой соответствуют аргументам вызова. В этом случае компилятор сообщает об ошибке отсутствия соответствия (no match). • Компилятор находит несколько функций, которые в принципе подходят, но ни одна из них не соответствует полностью. В этом случае компилятор также сообщает об ошибке, об ошибке неоднозначности вызова (ambiguous call).Упражнения раздела 6.4 Упражнение 6.39. Объясните результат второго объявления в каждом из следующих наборов. Укажите, какое из них (если есть) недопустимо. (a) int calc(int, int); int calc(const int, const int); (b) int get(); double get(); (c) int *reset(int *); double *reset(double *); 6.4.1. Перегрузка и область видимости Обычно объявлять функцию локально нежелательно. Но чтобы объяснить, как область видимости взаимодействует с перегрузкой, мы будем нарушать это правило и используем локальные объявление функции. Новички в программировании на языке С++ зачастую не понимают взаимодействия между областью видимости и перегрузкой. Однако у перегрузки нет никаких специальных свойств относительно области видимости. Как обычно, если имя объявлено во внутренней области видимости, оно скрывает (hidden name) такое же имя, объявленное во внешней области видимости. Имена не перегружают в областях видимости: string read(); void print(const string &); void print(double); // перегружает функцию print void fooBar(int ival) { bool read = false; // Page 303/1103 новая область видимости: скрывает // предыдущее объявление имени read string s = read(); // ошибка: read - переменная типа bool, а не // функция // плохой подход: обычно не следует объявлять функции в локальной // области видимости void print(int); // новая область видимости: скрывает предыдущие // экземпляры функции print print("Value: "); // ошибка: print(const string &) скрыта print(ival); // ok: print (int) видима print(3.14); // ok: вызов print(int); print(double) скрыта } Большинство читателей не удивит ошибка при вызове функции read(). Когда компилятор обрабатывает вызов функции read(), он находит локальное определение имени read. Это имя принадлежит переменной типа bool, а не функции. Следовательно, вызов некорректен. Точно тот же процесс используется при распознавании вызова функции print(). Объявление print(int) в функции fooBar скрывает прежнее ее объявление. В результате будет доступна только одна функция print(), та, которая получает один параметр типа int. Когда происходит вызов функции print(), компилятор ищет сначала объявление этого имени. Он находит локальное объявление функции print(), получающей один параметр типа int. Как только имя найдено, компилятор игнорирует такое же имя в любой внешней области видимости. Он полагает данное объявление единственно доступным для использования. Остается лишь удостовериться в допустимости использования этого имени. В языке С++ поиск имени осуществляется до проверки соответствия типов. Page 304/1103 Первый вызов передает функции print() строковый литерал, но единственное ее объявление, находящееся в области видимости, имеет параметр типа int. Строковый литерал не может быть преобразован в тип int, поэтому вызов ошибочен. Функция print(const string&), которая соответствовала бы этому вызову, скрыта и не рассматривается. Когда происходит вызов функции print() с передачей аргумента типа double(), процесс повторяется. Компилятор находит локальное определение функции print(int). Но аргумент типа double может быть преобразован в значение типа int, поэтому вызов корректен. Если бы объявление print(int) находилось в той же области видимости, что и объявления других версий функции print(), это была бы еще одна ее перегруженная версия. В этом случае вызовы распознавались бы по-другому, поскольку компилятор видел бы все три функции: void print(const string &); void print(double); // перегружает функцию print void print(int); // еще один экземпляр перегрузки void fooBar2(int ival) { print("Value: "); // вызов print(const string &) print(ival); // вызов print(int) print(3.14); // вызов print(double) } 6.5. Специальные средства В этом разделе рассматриваются три связанных с функциями средства, которые полезны во многих, но не во всех программах: аргументы по умолчанию, встраиваемые функции и функции constexpr, а также некоторые другие средства, обычно используемые во время отладки. 6.5.1. Аргументы по умолчанию Параметры некоторых функций могут обладать конкретными значениями, используемыми в большинстве, но не во всех вызовах. Такие обычно используемые значения называют Page 305/1103 аргументом по умолчанию (default argument). Функции с аргументами по умолчанию могут быть вызваны с ними или без них. Например, для представления содержимого окна можно было бы использовать тип string. Мы могли бы хотеть, чтобы по умолчанию у окна была определенная высота, ширина и фоновый символ. Но мы могли бы также захотеть позволить пользователям использовать собственные значения, кроме значений по умолчанию. Чтобы приспособить и значение по умолчанию, и определяемое пользователем, мы объявили бы функцию, представляющую окно, следующим образом: typedef string::size_type sz; // typedef см. p. 2.5.1 string screen(sz ht = 24, sz wid = 80, char backgrnd = ' '); Здесь мы предоставили для каждого параметра значение по умолчанию. Аргумент по умолчанию определяется как инициализатор параметра в списке параметров. Значения по умолчанию можно определить как для одного, так и для нескольких параметров. Но если у параметра есть аргумент по умолчанию, то все параметры, следующие за ним, также должны иметь аргументы по умолчанию. Вызов функции с аргументами по умолчанию Если необходимо использовать аргумент по умолчанию, его значение при вызове функции пропускают. Поскольку функция screen() предоставляет значения по умолчанию для всех параметров, мы можем вызвать ее без аргументов, с одним, двумя или тремя аргументами: string window; window = screen(); // эквивалент screen(24, 80, ' ') window = screen(66); // эквивалент screen(66, 80, ' ') window = screen(66, 256); // screen(66, 256, ' ') window = screen(66, 256, '#'); // screen(66, 256, '#') Аргументы в вызове распознаются по позиции. Значения по умолчанию используются для аргументов, крайних справа. Например, чтобы переопределить значение по умолчанию параметра background, следует поставить также аргументы для параметров height и width: window = screen(, , '?'); // ошибка: можно пропустить аргументы только // крайние справа window = screen('?'); // вызов screen('?', 80, ' ') Обратите внимание, что второй вызов, передающий одно символьное значение, вполне допустим. Несмотря на допустимость, это вряд ли то, что ожидалось. Вызов допустим потому, Page 306/1103 что символ '?' имеет тип char, а он может быть преобразован в тип крайнего левого параметра. Это параметр типа string::size_type, который является целочисленным беззнаковым типом. В этом вызове аргумент типа char неявно преобразуется в тип string::size_type и передается как аргумент параметру height. На машине авторов символ '?' имеет шестнадцатеричное значение 0x3F, соответствующее десятичному 63. Таким образом, этот вызов присваивает параметру height значение 63. Одной из задач при разработке функции с аргументами по умолчанию является упорядочивание параметров так, чтобы те из них, для которых использование значения по умолчанию вероятней всего, располагались последними. Объявление аргумента по умолчанию Хотя вполне обычной практикой является объявление функции однажды в заголовке, вполне допустимо многократно объявлять ее повторно. Однако у каждого параметра может быть свое значение по умолчанию, определенное только однажды в данной области видимости. Таким образом, любое последующее объявление может добавить значение по умолчанию только для того параметра, у которого ранее не было определено значение по умолчанию. Как обычно, значения по умолчанию могут быть определены, только если у всех параметров справа уже есть значения по умолчанию. Рассмотрим следующий пример: // у параметров height и width нет значений по умолчанию string screen(sz, sz, char = ' '); Нельзя изменить уже заявленное значение по умолчанию: string screen(sz, sz, char = '*'); // ошибка: переобъявление Но можно добавить аргумент по умолчанию следующим образом: string screen(sz = 24, sz = 80, char); // ok: добавление аргументов // по умолчанию Обычно аргументы по умолчанию определяют при объявлении функции в соответствующем заголовке. Инициализация аргумента по умолчанию Локальные переменные не могут использоваться как аргумент по умолчанию. За исключением этого ограничения, аргумент по умолчанию может быть любым выражением, тип которого приводим к типу параметра: // объявления wd, def и ht должны располагаться вне функции sz wd = 80; char def = ' '; sz ht(); Page 307/1103 string screen(sz = ht(), sz = wd, char = def); string window = screen(); // вызов screen(ht(), 80, ' ') Поиск имен, используемых для аргументов по умолчанию, осуществляется в пределах объявления функции. Значения, представляемые этими именами, вычисляются во время вызова: void f2() { def = '*'; // изменение значения аргумента по умолчанию sz wd = 100; // скрывает внешнее определение wd, но не изменяет // значение по умолчанию window = screen(); // вызов screen(ht(), 80, '*') } В функции f2() было изменено значение def. Вызов функции screen передает это измененное значение. Эта функция также объявляет локальную переменную, которая скрывает внешнюю переменную wd. Однако локальное имя wd никак не связано с аргументом по умолчанию, переданным функции screen(). Упражнения раздела 6.5.1 |