Передача данных через параметры и через глобальные объекты
Различные функции программы могут общаться между собой с помощью двух механизмов. (Под словом “общаться” мы подразумеваем обмен данными.) В одном случае используются глобальные объекты, в другом – передача параметров и возврат значений.
Глобальный объект определен вне функции. Например:
int glob;
int main() {
// что угодно
| }
Объект glob является глобальным. (В главе 8 рассмотрение глобальных объектов и глобальной области видимости будет продолжено.) Главное достоинство и одновременно один из наиболее заметных недостатков такого объекта – доступность из любого места программы, поэтому его обычно используют для общения между разными модулями. Обратная сторона медали такова:
· функции, использующие глобальные объекты, зависят от этих объектов и их типов. Использовать такую функцию в другом контексте затруднительно;
· при модификации такой программы повышается вероятность ошибок. Даже для внесения локальных изменений необходимо понимание всей программы в целом;
· если глобальный объект получает неверное значение, ошибку нужно искать по всей программе. Отсутствует локализация;
· используя глобальные объекты, труднее писать рекурсивные функции (Рекурсия возникает тогда, когда функция вызывает сама себя. Мы рассмотрим это в разделе 7.5.);
· если используются потоки (threads), то для синхронизации доступа к глобальным объектам требуется писать дополнительный код. Отсутствие синхронизации – одна из распространенных ошибок при использовании потоков. (Пример использования потоков при программировании на С++ см. в статье “Distributing Object Computing in C++” (Steve Vinoski and Doug Schmidt) в [LIPPMAN96b].)
Можно сделать вывод, что для передачи информации между функциями предпочтительнее пользоваться параметрами и возвращаемыми значениями.
Вероятность ошибок при таком подходе возрастает с увеличением списка. Считается, что восемь параметров – это приемлемый максимум. В качестве альтернативы длинному списку можно использовать в качестве параметра класс, массив или контейнер. Он способен содержать группу значений.
Аналогично программа может возвращать только одно значение. Если же логика требует нескольких, некоторые параметры объявляются ссылками, чтобы функция могла непосредственно модифицировать значения соответствующих фактических аргументов и использовать эти параметры для возврата дополнительных значений, либо некоторый класс или контейнер, содержащий группу значений, объявляется типом, возвращаемым функцией.
Упражнение 7.9
Каковы две формы инструкции return? Объясните, в каких случаях следует использовать первую, а в каких вторую форму.
Упражнение 7.10
Найдите в данной функции потенциальную ошибку времени выполнения:
vector<string> &readText( ) {
vector<string> text;
string word;
while ( cin >> word ) {
text.push_back( word );
// ...
}
// ....
return text;
| }
Упражнение 7.11
Каким способом вы вернули бы из функции несколько значений? Опишите достоинства и недостатки вашего подхода.
Рекурсия
Функция, которая прямо или косвенно вызывает сама себя, называется рекурсивной. Например:
int rgcd( int vl, int v2 )
{
if ( v2 != 0 )
return rgcd( v2, vl%v2 );
return vl;
| }
Такая функция обязательно должна определять условие окончания, в противном случае рекурсия будет продолжаться бесконечно. Подобную ошибку так иногда и называют – бесконечная рекурсия. Для rgcd() условием окончания является равенство нулю остатка.
Вызов
rgcd( 15, 123 );
возвращает 3 (см. табл. 7.1).
Таблица 7.1. Трассировка вызова rgcd (15,123)
vl
| v2
| return
|
|
| rgcd(123,15)
|
|
| rgcd(15,3)
|
|
| rgcd(3,0)
|
|
|
|
Последний вызов,
rgcd(3,0);
удовлетворяет условию окончания. Функция возвращает наибольший общий делитель, он же возвращается и каждым предшествующим вызовом. Говорят, что значение всплывает (percolates) вверх, пока управление не вернется в функцию, вызвавшую rgcd() в первый раз.
Рекурсивные функции обычно выполняются медленнее, чем их нерекурсивные (итеративные) аналоги. Это связано с затратами времени на вызов функции. Однако, как правило, они компактнее и понятнее.
Приведем пример. Факториалом числа n является произведение натуральных чисел от 1 до n. Так, факториал 5 равен 120: 1 ´ 2 ´ 3 ´ 4 ´ 5 = 120.
Вычислять факториал удобно с помощью рекурсивной функции:
unsigned long
factorial( int val ) {
if ( val > 1 )
return val * factorial( val-1 );
return 1;
| }
Рекурсия обрывается по достижении val значения 1.
Упражнение 7.12
Перепишите factorial() как итеративную функцию.
Упражнение 7.13
Что произойдет, если условием окончания factorial() будет следующее:
if ( val != 0 )
Встроенные функции
Рассмотрим следующую функцию min():
int min( int vl, int v2 )
{
return( vl < v2 ? vl : v2 );
| }
Преимущества определения функции для такой небольшой операции таковы:
· как правило, проще прочесть и интерпретировать вызов min(), чем читать условный оператор и вникать в смысл его действий, особенно если v1 и v2 являются сложными выражениями;
· модифицировать одну локализованную реализацию в приложении легче, чем 300. Например, если будет решено изменить проверку на:
( vl == v2 || vl < v2 )
поиск каждого ее вхождения будет утомительным и с большой долей вероятности приведет к ошибкам;
· семантика единообразна. Все проверки выполняются одинаково;
· функция может быть повторно использована в другом приложении.
Однако этот подход имеет один недостаток: вызов функции происходит медленнее, чем непосредственное вычисление условного оператора. Необходимо скопировать два аргумента, запомнить содержимое машинных регистров и передать управление в другое место программы. Решение дают встроенные функции. Встроенная функция “подставляется по месту” в каждой точке своего вызова. Например:
int minVa12 = min( i, j );
заменяется при компиляции на
int minVal2 = i < j ? i : j;
Таким образом, не требуется тратить время на реализацию min() в виде функции.
Функция min() объявляется как встроенная с помощью ключевого слова inline перед типом возвращаемого значения в объявлении или определении:
inline int min( int vl, int v2 ) { /* ... */ }
Заметим, однако, что спецификация inline – это только подсказка компилятору. Компилятор может проигнорировать ее, если функция плохо подходит для встраивания по месту. Например, рекурсивная функция (такая, как rgcd()) не может быть полностью встроена в месте вызова (хотя для самого первого вызова это возможно). Функция из 1200 строк также скорее всего не подойдет. В общем случае такой механизм предназначен для оптимизации небольших, простых, часто используемых функций. Он крайне важен для поддержки концепции сокрытия информации при разработке абстрактных типов данных. Например, встроенной объявлена функция-член size() в классе IntArray из раздела 2.3.
Встроенная функция должна быть видна компилятору в месте вызова. В отличие от обычной, такая функция определяется в каждом исходном файле, где есть обращения к ней. Конечно же, определения одной и той же встроенной функции в разных файлах должны совпадать. Если программа содержит два исходных файла compute.C и draw.C, не нужно писать для них разные реализации функции min(). Если определения функции различаются, программа становится нестабильной: неизвестно, какое из них будет выбрано для каждого вызова, если компилятор не стал встраивать эту функцию.
Рекомендуется помещать определение встроенной функции в заголовочный файл и включать его во все файлы, где есть обращения к ней. Такой подход гарантирует, что для встроенной функции существует только одно определение и код не дублируется; дублирование может привести к непреднамеренному расхождению текстов в течение жизненного цикла программы.
Поскольку min() является общеупотребительной операцией, реализация ее входит в стандартную библиотеку С++; это один из обобщенных алгоритмов, описанных в главе 12 и в Приложении. Функция min() реализована как шаблон, что позволяет ей работать с операндами арифметического типа, отличного от int. (Шаблоны функций рассматриваются в главе 10.)
7.7. Директива связывания extern "C" A
Если программист хочет использовать функцию, написанную на другом языке, в частности на С, то компилятору нужно указать, что при вызове требуются несколько иные условия. Скажем, имя функции или порядок передачи аргументов различаются в зависимости от языка программирования.
Показать, что функция написана на другом языке, можно с помощью директивы связывания в форме простой либо составной инструкции:
// директива связывания в форме простой инструкции
extern "C" void exit(int);
// директива связывания в форме составной инструкции
extern "C" {
int printf( const char* ... );
int scanf( const char* ... );
}
// директива связывания в форме составной инструкции
extern "C" {
#include <cmath>
| }
Первая форма такой директивы состоит из ключевого слова extern, за которым следует строковый литерал, а за ним – “обычное” объявление функции. Хотя функция написана на другом языке, проверка типов вызова выполняется полностью. Несколько объявлений функций могут быть помещены в фигурные скобки составной инструкции директивы связывания – второй формы этой директивы. Скобки отмечают те объявления, к которым она относится, не ограничивая их видимости, как в случае обычной составной инструкции. Составная инструкция extern "C" в предыдущем примере говорит только о том, что функции printf() и scanf() написаны на языке С. Во всех остальных отношениях эти объявления работают точно так же, как если бы они были расположены вне инструкции.
Если в фигурные скобки составной директивы связывания помещается директива препроцессора #include, все объявленные во включаемом заголовочном файле функции рассматриваются как написанные на языке, указанном в этой директиве. В предыдущем примере все функции из заголовочного файла cmath написаны на языке С.
Директива связывания не может появиться внутри тела функции. Следующий фрагмент кода вызывает ошибку компиляции:
int main() {
// ошибка: директива связывания не может появиться
// внутри тела функции
extern "C" double sqrt( double );
double getValue(); //правильно
double result = sqrt ( getValue() );
//...
return 0;
| }
Если мы переместим директиву так, чтобы она оказалась вне тела main(), программа откомпилируется правильно:
extern "C" double sqrt( double );
int main() {
double getValue(); //правильно
double result = sqrt ( getValue() );
//...
return 0;
| }
Однако более подходящее место для директивы связывания – заголовочный файл, где находится объявление функции, описывающее ее интерфейс.
Как сделать С++ функцию доступной для программы на С? Директива extern "C" поможет и в этом:
// функция calc() может быть вызвана из программы на C
| extern "C" double calc( double dparm ) { /* ... */ }
Если в одном файле имеется несколько объявлений функции, то директива связывания может быть указана при каждом из них или только при первом – в этом случае она распространяется и на все последующие объявления. Например:
//----myMath.h ----
extern "C" double calc( double );
// ---- myMath.C ----
// объявление calc() в myMath.h
#include "myMath.h"
// определение функции extern "C" calc()
// функция calc() может быть вызвана из программы на C
| double calc( double dparm ) { // ... }
В данном разделе мы видели примеры директивы связывания extern "C" только для языка С. Это единственный внешний язык, поддержку которого гарантирует стандарт С++. Конкретная реализация может поддерживать связь и с другими языками. Например, extern "Ada" для функций, написанных на языке Ada; extern "FORTRAN" для языка FORTRAN и т.д. Мы описали один из случаев использования ключевого слова extern в С++. В разделе 8.2 мы покажем, что это слово имеет и другое назначение в объявлениях функций и объектов.
Упражнение 7.14
exit(), printf(), malloc(), strcpy() и strlen() являются функциями из библиотеки С. Модифицируйте приведенную ниже С-программу так, чтобы она компилировалась и связывалась в С++.
const char *str = "hello";
void *malloc( int );
char *strcpy( char *, const char * );
int printf( const char *, ... );
int exit( int );
int strlen( const char * );
int main()
{ /* программа на языке С */
char* s = malloc( strlen(str)+l );
strcpy( s, str );
printf( "%s, world\n", s );
exit( 0 );
| }
Не нашли, что искали? Воспользуйтесь поиском по сайту:
©2015 - 2024 stydopedia.ru Все материалы защищены законодательством РФ.
|