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

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

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

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

312

void iStack::display()

{

cout << "( " << size() << " )( bot: "; for ( int ix=0; ix < size(); ++ix )

cout << _stack[ ix ] << " "; cout << " stop )\n";

}

Наиболее существенным изменениям подвергнется конструктор iStack. Никаких действий от него теперь не требуется. Можно было бы определить пустой конструктор:

inline iStack::iStack() {}

Однако это не совсем приемлемо для пользователей нашего класса. До сих пор мы строго сохраняли интерфейс класса iStack, и если мы хотим сохранить его до конца, необходимо оставить для конструктора один необязательный параметр. Вот как будет

class iStack { public:

iStack( int capacity = 0 ); // ...

выглядеть объявление конструктора с таким параметром типа int:

};

inline iStack::iStack( int capacity )

{

if ( capacity ) _stack.reserve( capacity );

Что делать с аргументом, если он задан? Используем его для указания емкости вектора:

}

Превращение класса в шаблон еще проще, в частности потому, что лежащий в основе

#include <vector>

template <class elemType> class Stack {

public:

Stack( int capacity=0 ); bool pop( elemType &value ); bool push( elemType value );

bool full(); bool empty(); void display();

int size(); private:

vector< elemType > _stack;

вектор сам является шаблоном. Вот модифицированное объявление:

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

313

};

Для обеспечения совместимости с программами, использующими наш прежний класс iStack, определим следующий typedef:

typedef Stack<int> iStack;

Модификацию операторов класса мы оставим читателю для упражнения.

Упражнение 6.29

Модифицируйте функцию peek() (упражнение 4.23 из раздела 4.15) для шаблона класса

Stack.

Упражнение 6.30

Модифицируйте операторы для шаблона класса Stack. Запустите тестовую программу из раздела 4.15 для новой реализации

Упражнение 6.31

По аналогии с классом List из раздела 5.11.1 инкапсулируйте наш шаблон класса Stack

в пространство имен Primer_Third_Edition

Часть III

Процедурно-ориентированное

программирование

В части II были представлены базовые компоненты языка С++: встроенные типы данных (int и double), типы классов (string и vector) и операции, которые можно совершать над данными. В части III мы увидим, как из этих компонентов строятся функции, служащие для реализации алгоритмов.

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

Функции используются для того, чтобы организовать программу в виде совокупности небольших и не зависящих друг от друга частей. Она инкапсулирует алгоритм или набор алгоритмов, применяемых к некоторому набору данных. Объекты и типы можно определить так, что они будут использоваться в течение всего времени работы программы. Однако, если некоторые объекты или типы применяются только в части программы, предпочтительнее ограничить область их использования именно этой частью и объявить внутри той функции, где они нужны. Понятие видимости предоставляет в распоряжение программиста механизм, позволяющий ограничивать область применения объектов. Различные области видимости, поддерживаемые языком С++, мы рассмотрим в главе 8.

Для облегчения использования функций С++ предлагает множество средств, рассматриваемых нами в части III. Первым из них является перегрузка. Функции, которые выполняют семантически одну и ту же операцию, но работают с разными типами данных и потому имеют несколько отличающиеся реализации, могут иметь общее имя. Например, все функции для печати значений разных типов, таких, как int, string и т.д., называются print(). Поскольку программисту не приходится запоминать много

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

314

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

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

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

оказаться недостаточным при возникновении непредвиденной ситуации в работе программы. Такие ситуации называются исключениями, и, поскольку они требуют немедленной реакции, необходимо иметь возможность послать сообщение вызывающей программе. Язык С++ предлагает механизм обработки исключений, который позволяет функциям общаться между собой в таких условиях. Этот механизм рассматривается в главе 11.

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

7. Функции

Мы рассмотрели, как объявлять переменные (глава 3), как писать выражения (глава 4) и инструкции (глава 5). Здесь мы покажем, как группировать эти компоненты в определения функций, чтобы облегчить их многократное использование внутри программы. Мы увидим, как объявлять и определять функции и как вызывать их, рассмотрим различные виды передаваемых параметров и обсудим особенности использования каждого вида. Мы расскажем также о различных видах значений, которые может вернуть функция. Будут представлены четыре специальных случая применения функций: встроенные (inline), рекурсивные, написанные на других языках и объявленные директивами связывания, а также функция main(). В завершение главы мы разберем более сложное понятие указатель на функцию.

7.1. Введение

Функцию можно рассматривать как операцию, определенную пользователем. В общем случае она задается своим именем. Операнды функции, или формальные параметры, задаются в списке параметров, через запятую. Такой список заключается в круглые скобки. Результатом функции может быть значение, которое называют возвращаемым. Об отсутствии возвращаемого значения сообщают ключевым словом void. Действия, которые производит функция, составляют ее тело; оно заключено в фигурные скобки. Тип возвращаемого значения, ее имя, список параметров и тело составляют определение функции. Вот несколько примеров:

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

315

inline int abs( int obj )

{

// возвращает абсолютное значение iobj return( iobj < 0 ? -iobj : iobj );

}

inline int min( int p1, int p2 )

{

// возвращает меньшую из двух величин return( pi < p2 ? pi : p2 );

}

int gcd( int vl, int v2 )

{

// возвращает наибольший общий делитель while ( v2 )

{

int temp = v2; v2 = vl % v2; vl = temp;

}

return vl;

}

Выполнение функции происходит тогда, когда в тексте программы встречается оператор вызова. Если функция принимает параметры, при ее вызове должны быть указаны фактические параметры, аргументы. Их перечисляют внутри скобок, через запятую. В следующем примере main() дважды вызывает abs() и по одному разу min() и gcd().

#include <iostream>

int main()

{

// прочитать значения из стандартного ввода cout << "Введите первое значение: ";

int i; cin >> i;

if ( !cin ) {

cerr << "!? Ошибка ввода - аварийный выход!\n"; return -1;

}

cout << "Введите второе значение: "; int j;

cin >> j;

if ( !cin ) {

cerr << "!? Ошибка ввода - аварийный выход!\n"; return -2;

}

cout << "\nmin: " << min( i, j ) << endl; i = abs( i );

j = abs( j );

cout << "НОД: " << gcd( i, j ) << endl; return 0;

Функция main() определяется в файле main.C.

}

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

316

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

Функция должна быть объявлена до момента ее вызова, попытка использовать необъявленное имя приводит к ошибке компиляции. Определение функции может служить ее объявлением, но ему разрешено появиться в программе только один раз. Поэтому обычно его помещают в отдельный исходный файл. Иногда в одном файле находятся определения нескольких функций, логически связанных друг с другом. Чтобы использовать их в другом исходном файле, необходим механизм, позволяющий объявить ее, не определяя.

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

В нашем примере файл main.C не содержит определений abs(), min() и gcd(), поэтому вызов любой из них приводит к ошибке компиляции. Чтобы компиляция была успешной,

int abs( int );

int min( int, int );

их необязательно определять, достаточно только объявить: int gcd( int, int );

(В таком объявлении можно не указывать имя параметра, ограничиваясь названием типа.)

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

// определение функции находится в файле gcd.С int gcd( int, int );

inline int abs(int i) { return( i<0 ? -i : i );

}

inline int min(int vl.int v2) { return( vl<v2 ? vl : v2 );

заголовочный файл для нашего примера. Назовем его localMath.h:

}

17 Таким образом, как мы видим, определения встроенных функций могут встретиться в программе несколько раз! – Прим. ред.

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

317

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

При выполнении наша программа main.C, получив от пользователя значения:

Введите первое значение: 15 Введите второе значение: 123

выдаст следующий результат:

mm: 15

НОД: 3

7.2. Прототип функции

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

7.2.1. Тип возвращаемого функцией значения

Тип возвращаемого функцией значения бывает встроенным, как int или double, составным, как int& или double*, или определенным пользователем перечислением или классом. Можно также использовать специальное ключевое слово void, которое

#include <string>

#include <vector> class Date { /* определение */ };

bool look_up( int *, int ); double calc( double );

int count( const string &, char ); Date& calendar( const char );

говорит о том, что функция не возвращает никакого значения: void sum( vector<int>&, int );

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

// массив не может быть типом возвращаемого значения

Следующий пример ошибочен: int[10] foo_bar();

Но можно вернуть указатель на первый элемент массива:

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

318

// правильно: указатель на первый элемент массива

int *foo_bar();

(Размер массива должен быть известен вызывающей программе.)

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

Функция может возвращать типы классов, в частности контейнеры. Например: list<char> foo_bar();

(Этот подход не очень эффективен. Обсуждение типа возвращаемого значения см. в разделе 7.4.)

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

// ошибка: пропущен тип возвращаемого значения

код вызывает ошибку компиляции:

const is_equa1( vector<int> vl, vector<int> v2 );

В предыдущих версиях С++ в подобных случаях считалось, что функция возвращает значение типа int. Стандарт С++ отменил это соглашение. Правильное объявление

// правильно: тип возвращаемого значения указан

is_equal() выглядит так:

const bool is_equa1( vector<int> vl, vector<int> v2 );

7.2.2. Список параметров функции

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

int fork();

Например, следующие объявления эквивалентны: int fork( void );

Такой список состоит из названий типов, разделенных запятыми. После имени типа может находиться имя параметра, хотя это и необязательно. В списке параметров не разрешается использовать сокращенную запись, соотнося одно имя типа с несколькими

int manip( int vl, v2 );

// ошибка

параметрами:

int manip( int vl, int v2 ); // правильно

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

319

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

void print( int *array, int size );

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

С++ допускает сосуществование двух или более функций, имеющих одно и то же имя, но разные списки параметров. Такие функции называются перегруженными. О списке параметров в этом случае говорят как о сигнатуре функции, поскольку именно он используется различения разных версий одноименных функций. Имя и сигнатура однозначно идентифицируют версию. (Перегруженные функции подробно обсуждаются в главе 9.)

7.2.3. Проверка типов формальных параметров

Функция gcd() объявлена следующим образом:

int gcd( int, int );

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

Что будет, если попытаться вызвать функцию gcd() с аргументами типа char*?

gcd( "hello", "world" );

А если передать этой функции не два аргумента, а только один? Или больше двух? Что случится, если потеряется запятая между числами 24 и 312?

gcd( 24312 );

Единственное разумное поведение компилятора сообщение об ошибке, поскольку попытка выполнить такую программу чревата весьма серьезными последствиями. С++ действительно не пропустит подобные вызовы. Текст сообщения будет выглядеть примерно так:

// gcd( "hello", "world" )

error: invalid argument types ( const char *, const char * ) -- expecting ( int, int )

ошибка: неверные типы аргументов ( const char *, const char * ) -- ожидается ( int, int )

// gcd( 24312 )

error: missing value for second argument

ошибка: пропущено значение второго аргумента

А если вызвать эту функцию с аргументами типа double? Должен ли этот вызов расцениваться как ошибочный?

gcd( 3.14, 6.29 );

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

320

Как было сказано в разделе 4.14, значение типа double может быть преобразовано в int. Следовательно, считать такой вызов ошибочным было бы слишком сурово. Вместо этого аргументы неявно преобразуются в int (отбрасыванием дробной части) и таким образом требования, налагаемые на типы параметров, выполняются. Поскольку при подобном преобразовании возможна потеря точности, хороший компилятор выдаст предупреждение. Вызов превращается в

gcd( 3, 6 );

что дает в результате 3.

С++ является строго типизированным языком. Компилятор проверяет аргументы на соответствие типов в каждом вызове функции. Если тип фактического аргумента не соответствует типу формального параметра, то производится попытка неявного преобразования. Если же это оказывается невозможным или число аргументов неверно, компилятор выдает сообщение об ошибке. Именно поэтому функция должна быть объявлена до того, как программа впервые обратится к ней: без объявления компилятор не обладает информацией для проверки типов.

Пропуск аргумента при вызове или передача аргумента неуказанного типа часто служили источником ошибок в языке С. Теперь такие погрешности обнаруживаются на этапе компиляции.

Упражнение 7.1

(a)set( int *, int );

(b)void func();

(c)string error( int );

Какие из следующих прототипов функций содержат ошибки? Объясните.

(d) arr[10] sum( int *, int );

Упражнение 7.2

Напишите прототипы для следующих функций:

Функция с именем compare, имеющая два параметра типа ссылки на класс matrix и возвращающая значение типа bool.

Функция с именем extract без параметров, возвращающая контейнер set для хранения значений типа int. (Контейнерный тип set описывался в разделе 6.13.)

Упражнение 7.3

double calc( double );

int count( const string &, char ); void sum( vector<int> &, int );

Имеются объявления функций: vector<int> vec( 10 );

Какие из следующих вызовов содержат ошибки и почему?

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

321

(a)calc( 23.4, 55.1 );

(b)count( "abcda", 'a' );

(c)sum( vec, 43.8 );

(d)calc( 66 );

7.3.Передача аргументов

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

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

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

Значения аргументов при передаче по значению не меняются. Следовательно,

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

Однако такой способ передачи аргументов может не устраивать нас в следующих случаях:

передача большого объекта типа класса. Временные и пространственные

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

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

// swap() не меняет значений своих аргументов! void swap( int vl, int v2 ) {

int tmp = v2; v2 = vl;

vl = tmp;

при передаче по значению:

}

swap() обменивает значения локальных копий своих аргументов. Те же переменные, что были использованы в качестве аргументов при вызове, остаются неизменными. Это можно проиллюстрировать, написав небольшую программу: