Главная страница

Язык программирования C Пятое издание


Скачать 1.85 Mb.
НазваниеЯзык программирования C Пятое издание
Дата15.07.2019
Размер1.85 Mb.
Формат файлаpdf
Имя файла620354-www.libfox.ru.pdf
ТипДокументы
#84130
страница17 из 54
1   ...   13   14   15   16   17   18   19   20   ...   54
Упражнение 6.40. Какое из следующих объявлений (если оно есть) содержит ошибку?
Почему?
(a) int ff(int a, int b = 0, int с = 0);
(b) char *init(int ht = 24, int wd, char bckgrnd);
Упражнение 6.41. Какие из следующих вызовов (если они есть) недопустимы? Почему? Какие из них допустимы (если они есть), но, вероятно, не соответствуют намерениям разработчика?
Почему? char *init(int ht, int wd = 80, char bckgrnd = ' ');
(a) init(); (b) init(24,10); (c) init(14, '*');
Упражнение 6.42. Присвойте второму параметру функции make_plural() (см. раздел 6.3.2)
аргумент по умолчанию 's'. Проверьте программу, выведя слова "success" и "failure" в единственном и множественном числе.
6.5.2. Встраиваемые функции и функции constexpr
Page 308/1103

В разделе 6.3.2 приведена небольшая функция, возвращающая ссылку на более короткую строку из двух переданных ей. К преимуществам определения функции для такой маленькой операции относятся следующие.
• Обращение к функции shorterString() проще и понятнее, чем эквивалентное условное выражение.
• Использование функции гарантирует одинаковое поведение. Она гарантирует, что каждая проверка будет выполнена тем же способом.
• Если придется внести изменение, проще сделать это в теле функции, а не выискивать в коде программы все случаи применения эквивалентного выражения.
• Функция может быть многократно использована при написании других приложений.
Однако у функции shorterString() есть один потенциальный недостаток: ее вызов происходит медленнее, чем вычисление эквивалентного выражения. На большинстве машин при вызове функции осуществляется довольно много действий: перед обращением сохраняются регистры, которые необходимо будет восстановить после выхода; происходит копирование значений аргументов; управление программой переходит к новому участку кода.
Встраиваемые функции позволяют избежать дополнительных затрат на вызов
Содержимое функции, объявленной встраиваемой (inline) при компиляции, как правило, встраивается по месту вызова.
Предположим, что функция shorterString() объявлена встраиваемой, а ее вызов имеет такой вид: cout << shorterString(s1, s2) << endl;
При компиляции тело функции окажется встроено по месту вызова, и в результате получится нечто вроде следующего: cout << (s1.size() < s2.size() ? s1 : s2) << endl;
Таким образом, во время выполнения удастся избежать дополнительных затрат, связанных с вызовом функции shorterString().
Чтобы объявить функцию shorterString() встраиваемой, в определении, перед типом возвращаемого значения, располагают ключевое слово inline.
// встраиваемая версия функции сравнения двух строк inline const string & shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2;
}
Объявление функции встраиваемой является только рекомендацией компилятору. Компилятор вполне может проигнорировать эту рекомендацию.
На самом деле механизм встраивания применяется в процессе оптимизации объектного
Page 309/1103
кода, в ходе которого код небольших функций, вызов которых происходит достаточно часто,
встраивается по месту вызова. Большинство компиляторов не будет встраивать рекурсивные функции. Функция на 75 строк также, вероятно, не будет встроена. Функции constexpr
Функция constexpr — это функция, которая может быть применена в константном выражении
(см. раздел 2.4.4). Функция constexpr определяется как любая другая функция, но должна соответствовать определенным ограничениям: возвращаемый тип и тип каждого параметра должны быть литералами (см. раздел 2.4.4), тело функции должно содержать только один оператор return: constexpr int new_sz() { return 42; } constexpr int foo = new_sz(); // ok: foo - константное выражение
Здесь функция new_sz определена как constexpr, она не получает никаких аргументов.
Компилятор может проверить (во время компиляции), что вызов функции new_sz()
возвращает константное выражение, поэтому ее можно использовать для инициализации переменной constexpr по имени foo.
Если это возможно, компилятор заменит вызов функции constexpr ее результирующим значением. Для этого функция constexpr неявно считается встраиваемой.
Тело функции constexpr может содержать другие операторы, если они не выполняют действий во время выполнения. Например, функция constexpr может содержать пустые операторы, псевдонимы типа (см. раздел 2.5.1) и объявления using.
Функции constexpr позволено возвратить значение, которое не является константой:
// scale(arg) - константное выражение, если arg - константное выражение constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
Функция scale() возвратит константное выражение, если ее аргумент будет константным выражением, но не в противном случае: int arr[scale(2)]; // ok: scale(2) - константное выражение int i = 2; // i - неконстантное выражение int a2[scale(i)]; // ошибка: scale(i) - неконстантное выражение
Если передать константное выражение (такое как литерал 2), возвращается тоже константное выражение. В данном случае компилятор заменит вызов функции scale() результирующим значением.
Если происходит вызов функции scale() с выражением, которое не является константным
(например, объект i типа int), то возвращается неконстантное выражение. Если использовать функцию scale() в контексте, требующем константного выражения, компилятор проверит,
Page 310/1103
является ли результат константным выражением. Если это не так, то компилятор выдаст сообщение об ошибке.
Функция constexpr не обязана возвращать константное выражение. Помещайте встраиваемые функции и функции constexpr в файлы заголовка
В отличие от других функций, встраиваемые функции и функции constexpr могут быть определены в программе несколько раз. В конце концов, чтобы встроить код, компилятор нуждается в определении, а не только в объявлении. Однако все определения конкретной встраиваемой функции и функции constexpr должны совпадать точно. В результате встраиваемые функции и функции constexpr обычно определяют в заголовках. Упражнения раздела 6.5.2
Упражнение 6.43. Какое из следующих объявлений и определений имеет смысл поместить в файл заголовка, а какой — в текст файла исходного кода? Объясните почему.
(a) inline bool eq(const BigInt&, const BigInt&) {...}
(b) void putValues(int *arr, int size);
Упражнение 6.44. Перепишите функцию isShorter() из раздела 6.2.2 как встраиваемую.
Упражнение 6.45. Пересмотрите функции, написанные для предыдущих упражнений, и решите, должны ли они быть определены как встраиваемые. Если да, то сделайте это. В
противном случае объясните, почему они не должны быть встраиваемыми.
Упражнение 6.46. Возможно ли определить функцию isShorter как constexpr? Если да, то сделайте это. В противном случае объясните, почему нет.
6.5.3. Помощь в отладке
Для условного выполнения отладочного кода программисты С++ иногда используют подход,
подобный защите заголовка (см. раздел 2.6.3). Идея в том, что программа будет содержать отладочный код, который выполняется только во время разработки программы. Когда приложение закончено и готово к выпуску, отладочный код исключается. Этот подход подразумевает использование двух средств препроцессора: assert и NDEBUG. Макрос препроцессора assert
Макрос assert — это макрос препроцессора (preprocessor macro). Макрос препроцессора — это переменная препроцессора, действующая как встраиваемая функция. Макрос assert получает одно выражение и использует его как условие: assert(выражение);
Если результат выражения ложь (т.е. нуль), то макрос assert выдает сообщение и закрывает программу. Если результат выражения — истина (т.е. он отличен от нуля), то макрос assert не делает ничего.
Действие макроса препроцессора подобно вызову функции. Макрос assert получает одно выражение , которое он использует как условие.
Page 311/1103

Макрос assert определен в заголовке cassert. Как уже упоминалось, относящиеся к препроцессору имена обрабатывает препроцессор, а не компилятор (см. раздел 2.3.2). В
результате такие имена можно использовать непосредственно, без объявления using. Таким образом, используется имя assert, а не std::assert, кроме того, для него не предоставляется объявление using.
Макрос assert зачастую используется для проверки "недопустимых" условий. Например,
программа обработки вводимого текста могла бы проверять, что все вводимые слова длиннее некоего порогового значения. Эта программа могла бы содержать такой оператор: assert(word.size() > threshold); Переменная препроцессора NDEBUG
Поведение макроса assert зависит от состояния переменной препроцессора NDEBUG. Если переменная NDEBUG определена, макрос assert ничего не делает. По умолчанию переменная NDEBUG не определена, поэтому по умолчанию макрос assert выполняет проверку.
Отладку можно "выключить", предоставив директиву #define, определяющую переменную
NDEBUG. В качестве альтернативы большинство компиляторов предоставляет параметр командной строки, позволяющий определять переменные препроцессора:
$ CC -D NDEBUG main.С # use /D with the Microsoft compiler
Результат будет тот же, что и при наличии строки #define NDEBUG в начале файла main.С.
Когда переменная NDEBUG определена, программа во время выполнения избегает дополнительных затрат на проверку различных условий. Самих проверок во время выполнения, конечно, тоже не будет. Поэтому макрос assert следует использовать только для проверки того, что действительно недопустимо. Это может быть полезно при отладке программы, но не должно использоваться для замены логических проверок времени выполнения или проверки ошибок, которые должна осуществлять программа.
В дополнение к макросу assert можно написать собственный отладочный код,
выполняющийся в зависимости от переменной NDEBUG. Если переменная NDEBUG не определена, код между директивами #ifndef и #endif выполняется, а в противном случае игнорируется: void print(const int ia[], size_t size) {
#ifndef NDEBUG
//
__func__ - локальная статическая переменная, определенная
// компилятором. Она содержит имя функции cerr << __func__ << ": array size is " << size << endl;
#endif
// ...
Здесь переменная __func__ используется для вывода имени отлаживаемой функции.
Компилятор определяет переменную __func__ в каждой функции. Это локальный статический
Page 312/1103
массив типа const char, содержащий имя функции.
Кроме переменной __func__, определяемой компилятором С++, препроцессор определяет четыре других имени, которые также могут пригодиться при отладке:
__FILE__ строковый литерал, содержащий имя файла.
__LINE__ целочисленный литерал, содержащий номер текущий строки.
__TIME__ строковый литерал, содержащий файл и время компиляции.
__DATE__ строковый литерал, содержащий файл и дату компиляции.
Эти константы можно использовать для отображения дополнительной информации в сообщениях об ошибках: if (word.size() < threshold) cerr << "Error: " << __FILE__
<< " : in function " << __func__
<< " at line " << __LINE__ << endl
<< " Compiled on " << __DATE__
<< " at " << __TIME__ << endl
<< " Word read was \"" << word << "\": Length too short" << endl;
Если передать этой программе строку, которая короче threshold, то будет создано следующее сообщение об ошибке:
Error: wdebug.cc : in function main at line 27
Compiled on Jul 11 2012 at 20:50:03
Word read was "foo": Length too short Упражнения раздела 6.5.3
Упражнение 6.47. Пересмотрите программу, написанную в упражнении раздела 6.3.2, где использовалась рекурсия для отображения содержимого вектора так, чтобы условно отображать информацию о ее выполнении. Например, отобразите размер вектора при каждом вызове. Откомпилируйте и запустите программу с включенной отладкой и с выключенной.
Упражнение 6.48. Объясните, что делает этот цикл и стоит ли использовать в нем макрос assert: string s; while (cin >> s && s != sought) { } // пустое тело assert(cin);
6.6. Подбор функции
Page 313/1103

Во многих (если не во всех) случаях довольно просто выяснить, какая из перегруженных версий функции будет использована при данном вызове. Но это не так просто, когда у перегруженных функций одинаковое количество параметров и когда один или несколько параметров имеют типы, связанные преобразованиями. Для примера рассмотрим следующий набор перегруженных функций и их вызов: void f() ; void f(int) ; void f(int, int); void f(double, double = 3.14); f(5.6); // вызов void f(double, double) Выявление кандидатов и подходящих функций
На первом этапе подбора перегруженной функции выявляют набор версий, подходящих для рассматриваемого вызова. Такие функции называются функциями-кандидатами (candidate function). Функция-кандидат имеет имя, указанное при вызове, и видима в точке вызова. В данном примере кандидатами являются все четыре функции по имени f.
На втором этапе выбора функции из набора кандидатов выявляются те, которые могут быть вызваны с аргументами данного вызова. Выбранные функции называют подходящими (viable function). Чтобы считаться подходящей, функция должна иметь столько же параметров, сколько аргументов передано при вызове, и тип каждого аргумента должен совпадать или допускать преобразование в тип соответствующего параметра.
При вызове f(5.6) две функции-кандидата можно исключить сразу из-за несоответствия количеству аргументов. Речь идет о версии без параметров и версии с двумя параметрами типа int. В данном случае вызов имеет только один аргумент, а эти функции не имеют их вообще или имеют два параметра соответственно.
Функция, получающая один аргумент типа int, и функция, получающая два аргумента типа double, могли бы быть подходящими. Любая из них может быть вызвана с одним аргументом.
Функция, получающая два аргумента типа double, имеет аргумент по умолчанию, а значит,
может быть вызвана с одним аргументом.
Когда у функции есть аргументы по умолчанию (см. раздел 6.5.1), при вызове может быть передано меньше аргументов, чем она фактически имеет.
После проверки количества аргументов, позволяющей выявить функции, подходящие потенциально, проверяется соответствие типов параметров функций типам аргументов,
переданных при вызове. Как и при любом обращении, тип аргумента может либо совпадать,
либо допускать преобразование в тип параметра. В данном случае подходят обе оставшиеся функции.
• Функция f(int) является подходящей потому, что аргумент типа double может быть неявно преобразован в параметр типа int.
• Функция f(double, double) также является подходящей потому, что для второго параметра задано значение по умолчанию, а первый параметр имеет тип double, который точно соответствует типу аргумента.
Если никаких подходящих функций не обнаружено, компилятор выдает сообщение об
Page 314/1103
ошибке. Поиск наилучшего соответствия, если он есть
На третьем этапе подбора перегруженной функции выясняется, какая из допустимых функций наилучшим образом соответствует вызову. Этот процесс анализирует каждый аргумент вызова и выбирает подходящую функцию (или функции), для которой соответствие параметра аргументу является наилучшим. Подробно критерии наилучшего соответствия рассматриваются в следующем разделе, а пока достаточно знать, что чем ближе типы аргумента и параметра друг к другу, тем лучше соответствие.
В данном случае существует только один (явный) аргумент, который имеет тип double. При вызове версии f(int) аргумент преобразуется из типа double в тип int. Вторая подходящая функция, f(double, double), точно соответствует типу этого аргумента. Поскольку точное соответствие лучше соответствия требующего преобразования, компилятор предпочитает версию с двумя параметрами типа double. Для второго, недостающего аргумента компилятор добавит аргумент по умолчанию. Подбор перегруженной версии с несколькими параметрами
Если у функции два или несколько аргументов, подбор подходящей версии усложняется.
Предположим, что функции имеют то же имя f, но анализируется следующий вызов: f(42, 2.56);
Набор подходящих функций выявляется, как прежде. Компилятор выбирает те версии функции, которые имеют необходимое количество параметров, типы которых соответствуют типам аргументов. В данном случае в набор подходящих вошли функции f(int, int) и f(double,
double). Затем компилятор перебирает аргументы один за одним и определяет, какая из версий функций имеет наилучшее соответствие. Наилучше соответствующая функция та, для которой единственной выполняются следующие условия.
• Соответствие по каждому аргументу не хуже, чем у остальных подходящих функций.
• По крайней мере у одного аргумента соответствие лучше, чем у остальных подходящих функций.
Если после просмотра всех аргументов не было найдено ни одной функции, которая считалась бы наилучше соответствующей, компилятор сообщает об ошибке неоднозначности вызова.
В рассматриваемом примере вызова анализ лишь первого аргумента для версии f(int, int)
функции f() обнаруживает точное соответствие. При анализе второй версии функции f()
оказывается, что аргумент 42 типа int следует преобразовать в значение типа double.
Соответствие в результате встроенного преобразования хуже, чем точное. Таким образом,
рассматривая только этот параметр, лучше соответствует та версия функции f(), которая обладает двумя параметрами типа int, а не двумя параметрами типа double.
Но при переходе ко второму аргументу оказывается, что версия функции f() с двумя параметрами типа double точно соответствует аргументу 2.56. Вызов версии функции f() с двумя параметрами типа int потребует преобразования аргумента 2.56 из типа double в тип int. Таким образом, при рассмотрении только второго параметра версия f(double, double)
функции f() имеет лучшее соответствие.
Компилятор отклонит этот вызов, поскольку он неоднозначен: каждая подходящая функция является лучшим соответствием по одному из аргументов. Было бы заманчиво обеспечить соответствие за счет явного приведения типов (см. раздел 4.11.3) одного из аргументов. Но в хорошо спроектированных системах в приведении аргументов не должно быть необходимости.
Page 315/1103

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

1   ...   13   14   15   16   17   18   19   20   ...   54


написать администратору сайта