Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
235
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

342

Matrix& grow( Matrix* p ) {

Matrix *res;

//выделим память для объекта Matrix

//большого размера

//res адресует этот новый объект

//скопируем содержимое *p в *res return *res;

}

Если возвращается большой объект, то гораздо эффективнее перейти от возврата по значению к использованию ссылки или указателя. В некоторых случаях компилятор может сделать это автоматически. Такая оптимизация получила название именованное возвращаемое значение. (Она описывается в разделе 14.8.)

Объявляя функцию как возвращающую ссылку, программист должен помнить о двух возможных ошибках:

∙ возврат ссылки на локальный объект, время жизни которого ограничено временем выполнения функции. (О времени жизни локальных объектов речь пойдет в разделе 8.3.) По завершении функции такой ссылке соответствует область

// ошибка: возврат ссылки на локальный объект

Matrix& add( Matrix &m1, Matrix &m2 )

{

Matrix result:

if ( m1.isZero() ) return m2;

if ( m2.isZero() ) return m1;

//сложим содержимое двух матриц

//ошибка: ссылка на сомнительную область памяти

//после возврата

return result;

памяти, содержащая неопределенное значение. Например:

}

В таком случае тип возврата не должен быть ссылкой. Тогда локальная переменная может быть скопирована до окончания времени своей жизни:

Matrix add( ... )

∙ функция возвращает l-значение. Любая его модификация затрагивает сам объект. Например:

С++ для начинающих

343

#include <vector>

int &get_val( vector<int> &vi, int ix ) { return vi [ix];

}

int ai[4] = { 0, 1, 2, 3 };

vector<int> vec( ai, ai+4 ); // копируем 4 элемента ai в vec

int main() {

//увеличивает vec[0] на 1 get_val( vec.0 )++;

//...

}

Для предотвращения нечаянной модификации возвращенного объекта нужно объявить тип возврата как const:

const int &get_val( ... )

Примером ситуации, когда l-значение возвращается намеренно, чтобы позволить модифицировать реальный объект, может служить перегруженный оператор взятия индекса для класса IntArray из раздела 2.3.

7.4.1. Передача данных через параметры и через глобальные объекты

Различные функции программы могут общаться между собой с помощью двух механизмов. (Под словом общатьсямы подразумеваем обмен данными.) В одном случае используются глобальные объекты, в другом передача параметров и возврат значений.

int glob; int main() {

// что угодно

Глобальный объект определен вне функции. Например:

}

Объект glob является глобальным. (В главе 8 рассмотрение глобальных объектов и глобальной области видимости будет продолжено.) Главное достоинство и одновременно один из наиболее заметных недостатков такого объекта доступность из любого места программы, поэтому его обычно используют для общения между разными модулями. Обратная сторона медали такова:

функции, использующие глобальные объекты, зависят от этих объектов и их типов. Использовать такую функцию в другом контексте затруднительно;

при модификации такой программы повышается вероятность ошибок. Даже для внесения локальных изменений необходимо понимание всей программы в целом;

С++ для начинающих

344

если глобальный объект получает неверное значение, ошибку нужно искать по всей программе. Отсутствует локализация;

используя глобальные объекты, труднее писать рекурсивные функции (Рекурсия возникает тогда, когда функция вызывает сама себя. Мы рассмотрим это в разделе 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

Каким способом вы вернули бы из функции несколько значений? Опишите достоинства и недостатки вашего подхода.

С++ для начинающих

345

7.5. Рекурсия

Функция, которая прямо или косвенно вызывает сама себя, называется рекурсивной.

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

 

 

 

15

123

rgcd(123,15)

123

15

rgcd(15,3)

15

3

rgcd(3,0)

3

0

3

Последний вызов,

rgcd(3,0);

удовлетворяет условию окончания. Функция возвращает наибольший общий делитель, он же возвращается и каждым предшествующим вызовом. Говорят, что значение всплывает (percolates) вверх, пока управление не вернется в функцию, вызвавшую rgcd() в первый раз.

Рекурсивные функции обычно выполняются медленнее, чем их нерекурсивные (итеративные) аналоги. Это связано с затратами времени на вызов функции. Однако, как правило, они компактнее и понятнее.

Приведем пример. Факториалом числа n является произведение натуральных чисел от 1 до n. Так, факториал 5 равен 120: 1 × 2 × 3 × 4 × 5 = 120.

Вычислять факториал удобно с помощью рекурсивной функции:

С++ для начинающих

346

 

 

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 )

7.6. Встроенные функции

int min( int vl, int v2 )

{

return( vl < v2 ? vl : v2 );

Рассмотрим следующую функцию min():

}

Преимущества определения функции для такой небольшой операции таковы:

как правило, проще прочесть и интерпретировать вызов min(), чем читать условный оператор и вникать в смысл его действий, особенно если v1 и v2 являются сложными выражениями;

модифицировать одну локализованную реализацию в приложении легче, чем

300.Например, если будет решено изменить проверку на:

( vl == v2 || vl < v2 )

поиск каждого ее вхождения будет утомительным и с большой долей вероятности приведет к ошибкам;

семантика единообразна. Все проверки выполняются одинаково;

функция может быть повторно использована в другом приложении.

Однако этот подход имеет один недостаток: вызов функции происходит медленнее, чем непосредственное вычисление условного оператора. Необходимо скопировать два аргумента, запомнить содержимое машинных регистров и передать управление в другое место программы. Решение дают встроенные функции. Встроенная функция подставляется по местув каждой точке своего вызова. Например:

int minVa12 = min( i, j );

С++ для начинающих

347

заменяется при компиляции на

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

Если программист хочет использовать функцию, написанную на другом языке, в частности на С, то компилятору нужно указать, что при вызове требуются несколько иные условия. Скажем, имя функции или порядок передачи аргументов различаются в зависимости от языка программирования.

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

С++ для начинающих

348

//директива связывания в форме простой инструкции 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;

откомпилируется правильно:

С++ для начинающих

349

}

Однако более подходящее место для директивы связывания заголовочный файл, где находится объявление функции, описывающее ее интерфейс.

Как сделать С++ функцию доступной для программы на С? Директива 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() являются функциями из библиотеки С. Модифицируйте приведенную ниже С-программу так, чтобы она компилировалась и связывалась в С++.

С++ для начинающих

350

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 );

}

7.8. Функция main(): разбор параметров командной

строки

При запуске программы мы, как правило, передаем ей информацию в командной строке. Например, можно написать

prog -d -o of lie dataO

Фактические параметры являются аргументами функции main() и могут быть получены из массива C-строк с именем argv; мы покажем, как их использовать.

Во всех предыдущих примерах определение main() содержало пустой список:

int main() { ... }

Развернутая сигнатура main() позволяет получить доступ к параметрам, которые были заданы пользователем в командной строке:

int main( int argc, char *argv[] ){...}

argc содержит их количество, а argv C-строки, представляющие собой отдельные значения (в командной строке они разделяются пробелами). Скажем, при запуске

команды

prog -d -o ofile data0

argc получает значение 5, а argv включает следующие строки:

argv[ 0 ] = "prog"; argv[ 1 ] = "-d"; argv[ 2 ] = "-o"; argv[ 3 ] = "ofile"; argv[ 4 ] = "dataO";

С++ для начинающих

351

В argv[0] всегда входит имя команды (программы). Элементы с индексами от 1 до argc-1 служат параметрами.

Посмотрим, как можно извлечь и использовать значения, помещенные в argv. Пусть

prog [-d] [-h] [-v]

[-o output_file] [-l limit_value] file_name

программа из нашего примера вызывается таким образом:

[ file_name [file_name [ ... ]]]

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

prog chap1.doc

prog -l 1024 -o chap1-2.out chapl.doc chap2.doc prog d chap3.doc

Но можно запускать и так: prog -l 512 -d chap4.doc

При разборе параметров командной строки выполняются следующие основные шаги:

1. По очереди извлечь каждый параметр из argv. Мы используем для этого цикл for с

for ( int ix = 1; ix < argc; ++ix ) { char *pchar = argv[ ix ];

// ...

начальным индексом 1 (пропуская, таким образом, имя программы):

}

2.Определить тип параметра. Если строка начинается с дефиса (-), это одна из опций { h, d, v, l, o}. В противном случае это может быть либо значение, ассоциированное с опцией (максимальный размер для -l, имя выходного файла для -o), либо имя входного файла. Чтобы определить, начинается ли строка с дефиса, используем

switch ( pchar[ 0 ] ) { case '-': {

//-h, -d, -v, -l, -o

}

default: {

//

обработаем максимальный размер

для

опции -1

//

имя выходного файла

для

-o

//имена входных файлов ...

}

инструкцию switch: