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

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

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

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

332

putValues() он заменяется на реальный тип int, double, string и т.д. (В главе 10 мы продолжим разговор о шаблонах функций.)

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

putValues( int matrix[][10], int rowSize );

Здесь matrix объявляется как двумерный массив, который содержит десять столбцов и неизвестное число строк. Эквивалентным объявлением для matrix будет:

int (*matrix)[10]

Многомерный массив передается как указатель на его нулевой элемент. В нашем случае тип matrix указатель на массив из десяти элементов типа int. Как и для одномерного массива, граница первого измерения не учитывается при проверке типов. Если параметры являются многомерными массивами, то контролируются все измерения, кроме первого.

Заметим, что скобки вокруг *matrix необходимы из-за более высокого приоритета операции взятия индекса. Инструкция

int *matrix[10];

объявляет matrix как массив из десяти указателей на int.

7.3.4. Абстрактные контейнерные типы в качестве параметров

Абстрактные контейнерные типы, представленные в главе 6, также используются для объявления параметров функции. Например, можно определить putValues() как имеющую параметр типа vector<int> вместо встроенного типа массива.

Контейнерный тип является классом и обеспечивает значительно большую функциональность, чем встроенные массивы. Так, vector<int> знаетсобственный размер. В предыдущем подразделе мы видели, что размер параметра-массива неизвестен функции и для его передачи приходится задавать дополнительный параметр. Использование vector<int> позволяет обойти это ограничение. Например, можно изменить определение нашей putValues() на такое:

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

333

#include <iostream> #include <vector>

const lineLength =12; // количество элементов в строке void putValues( vector<int> vec )

{

cout << "( " << vec.size() << " )< "; for ( int i = 0; i < vec.size(); ++1 ) {

if ( i % lineLength == 0 && i )

cout << "\n\t"; // строка заполнена

cout << vec[ i ];

//разделитель, печатаемый после каждого элемента,

//кроме последнего

if ( 1 % lineLength != lineLength-1 && i != vec.size()-1 )

cout << ", ";

}

cout << " >\n";

}

void putValues( vector<int> ); int main() {

int i, j[ 2 ];

// присвоить i и j некоторые значения

vector<int> vec1(1); // создадим вектор из 1 элемента vecl[0] = i;

putValues( vecl );

vector<int> vec2; // создадим пустой вектор // добавим элементы к vec2

for ( int ix = 0;

ix < sizeof( j ) / sizeof( j[0] ); ++ix )

// vec2[ix] == j [ix] vec2.push_back( j[ix] );

putValues( vec2 ); return 0;

Функция main(), вызывающая нашу новую функцию putValues(), выглядит так:

}

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

Как бы вы изменили объявление putValues()?

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

void putValues( const vector<int> & ) { ...

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

334

7.3.5. Значения параметров по умолчанию

Значение параметра по умолчанию это значение, которое разработчик считает подходящим в большинстве случаев употребления функции, хотя и не во всех. Оно

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

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

char *screenInit( int height = 24, int width = 80,

фона экрана:

char background = ' ' );

Функция, для которой задано значение параметра по умолчанию, может вызываться по- разному. Если аргумент опущен, используется значение по умолчанию, в противном случае значение переданного аргумента. Все следующие вызовы screenInit()

char *cursor;

//эквивалентно screenInit(24,80,' ') cursor = screenInit();

//эквивалентно screenInit(66,80,' ') cursor = screenlnit(66);

//эквивалентно screenInit(66,256,' ') cursor = screenlnit(66, 256);

корректны:

cursor = screenlnit(66, 256, '#');

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

//эквивалентно screenInit('?',80,' ') cursor = screenInit('?');

//ошибка, неэквивалентно screenInit(24,80,'?')

невозможно задать значение для background, не задавая его для height и width. cursor = screenInit( , ,'?');

При разработке функции с параметрами по умолчанию придется позаботиться об их расположении. Те, для которых значения по умолчанию вряд ли будут употребляться, необходимо поместить в начало списка. Функция screenInit() предполагает (возможно, основываясь на опыте применения), что параметр height будет востребован пользователем наиболее часто.

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

335

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

//ошибка: width должна иметь значение по умолчанию,

//если такое значение имеет height

char *screenlnit( int height = 24, int width,

указаны.

char background = ' ' );

Значение по умолчанию может указываться только один раз в файле. Следующая запись

// tf.h

int ff( int = 0 );

// ft.С #include "ff.h"

ошибочна:

int ff( int i = 0) { ... } // ошибка

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

Можно объявить функцию повторно и таким образом задать дополнительные параметры по умолчанию. Это удобно при настройке универсальной функции для конкретного приложения. Скажем, в системной библиотеке UNIX есть функция chmod(), изменяющая режим доступа к файлу. Ее объявление содержится в системном заголовочном файле

<cstdlib>:

int chmod( char *filePath, int protMode );

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

#include <cstdlib>

при каждом вызове:

int chmod( char *filePath, int protMode=0444 );

Если функция объявлена в заголовочном файле так:

file int ff( int a, int b, int с = 0 ); // ff.h

то как переобъявить ее, чтобы присвоить значение по умолчанию для параметра b? Следующая строка ошибочна, поскольку она повторно задает значение для с:

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

336

#include "ff.h"

int ff( int a, int b = 0, int с = 0 ); // ошибка

#include "ff.h"

Так выглядит правильное объявление:

int ff( int a, int b = 0, int с ); // правильно

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

#include "ff.h"

int ff( int a, int b = 0, int с ); // правильно

значения справа налево не нарушается. Теперь мы можем переобъявить ff() еще раз: int ff( int a = 0, int b, int с ); // правильно

Значение по умолчанию не обязано быть константным выражением, можно использовать

int aDefault();

int bDefault( int );

int cDefault( double = 7.8 );

int glob;

int ff( int a = aDefault() ,

int b = bDefau1t( glob ) ,

любое:

int с = cDefault() );

Если такое значение является выражением, то оно вычисляется во время вызова функции. В примере выше cDefault() работает каждый раз, когда происходит вызов функции ff() без указания третьего аргумента.

7.3.6. Многоточие

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

void foo( parm_list, ... );

Многоточие употребляется в двух форматах: void foo( ... );

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

337

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

Примером вынужденного использования многоточия служит функция printf() стандартной библиотеки С. Ее первый параметр является C-строкой:

int printf( const char* ... );

Это гарантирует, что при любом вызове printf() ей будет передан первый аргумент типа const char*. Содержание такой строки, называемой форматной, определяет, необходимы ли дополнительные аргументы при вызове. При наличии в строке формата метасимволов, начинающихся с символа %, функция ждет присутствия этих аргументов. Например, вызов

printf( "hello, world\n" );

имеет один строковый аргумент. Но

printf( "hello, %s\n", userName );

имеет два аргумента. Символ % говорит о наличии второго аргумента, а буква s, следующая за ним, определяет его тип в данном случае символьную строку.

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

void f();

Отметим, что следующие объявления неэквивалентны: void f( ... );

В первом случае f() объявлена как функция без параметров, во втором как имеющая

f( someValue );

ноль или более параметров. Вызовы f( cnt, a, b, с );

корректны только для второго объявления. Вызов

f();

применим к любой из двух функций. Упражнение 7.4

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

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

338

(a)void print( int arr[][], int size );

(b)int ff( int a, int b = 0, int с = 0 );

(d)

char *screenInit(

int height

= 24, int width,

 

char

background

);

(c) void operate( int *matrix[] );

(e)void putValues( int (&ia)[] );

Упражнение 7.5

(a) char *screenInit( int height, int width, char background = ' ' );

char *screenInit( int height = 24, int width, char background );

(b)void print( int (*arr)[6], int size ); void print( int (*arr)[5], int size );

(c)void manip( int *pi, int first, int end = 0 );

Повторные объявления всех приведенных ниже функций содержат ошибки. Найдите их. void manip( int *pi, int first = 0, int end = 0 );

Упражнение 7.6

void print( int arr[][5], int size ); void operate(int *matrix[7]);

char *screenInit( int height = 24, int width = 80,

Даны объявления функций.

char background = ' ' );

(a)screenInit();

(b)int *matrix[5]; operate( matrix );

(c)int arr[5][5];

Вызовы этих функций содержат ошибки. Найдите их и объясните. print( arr, 5 );

Упражнение 7.7

Перепишите функцию putValues( vector<int> ), приведенную в подразделе 7.3.4, так, чтобы она работала с контейнером list<string>. Печатайте по одному значению на строке. Вот пример вывода для списка из двух строк:

( 2 )

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

339

<

"first string" "second string"

>

Напишите функцию main(), вызывающую новый вариант putValues() со следующим

"put function declarations in header files"

"use abstract container types instead of built-in arrays" "declare class parameters as references"

"use reference to const types for invariant parameters"

списком строк:

"use less than eight parameters"

Упражнение 7.8

В каком случае вы применили бы параметр-указатель? А в каком параметр-ссылку? Опишите достоинства и недостатки каждого способа.

7.4. Возврат значения

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

return;

данная. Инструкция return может употребляться в двух формах: return expression;

Первая форма используется в функциях, для которых типом возвращаемого значения является void. Использовать return в таких случаях обязательно, если нужно принудительно завершить работу. (Такое применение return напоминает инструкцию break, представленную в разделе 5.8.) После конечной инструкции функции подразумевается наличие return. Например:

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

340

 

 

void d_copy( double "src, double *dst, int sz )

 

 

 

 

 

 

{

 

 

 

/* копируем массив "src" в "dst"

 

 

 

* для простоты предполагаем, что они одного размера

 

 

 

*/

 

 

 

// завершение, если хотя бы один из указателей равен 0

 

 

 

if ( !src || !dst )

 

 

 

return;

 

 

 

// завершение,

 

 

 

// если указатели адресуют один и тот же массив

 

 

 

if ( src == dst )

 

 

 

return;

 

 

 

// копировать нечего

 

 

 

if ( sz == 0 )

 

 

 

return;

 

 

 

// все еще не закончили?

 

 

 

// тогда самое время что-то сделать

 

 

 

for ( int ix = 0; ix < sz; ++ix )

 

 

 

dst[ix] = src[ix];

 

 

 

// явного завершения не требуется

 

 

 

}

 

 

 

 

 

 

 

Во второй форме инструкции return указывается то значение, которое функция должна

 

вернуть. Это значение может быть сколь угодно сложным выражением, даже содержать

 

вызов функции. В реализации функции factorial(), которую мы рассмотрим в

 

следующем разделе, используется return следующего вида:

 

 

 

return val * factorial(val-1);

 

 

 

 

 

В функции, не объявленная с void в качестве типа возвращаемого значения, обязательно

 

использовать вторую форму return, иначе произойдет ошибка компиляции. Хотя

 

компилятор не отвечает за правильность результата, он сможет гарантировать его

 

наличие. Следующая программа не компилируется из-за двух мест, где программа

 

завершается без возврата значения:

 

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

341

// определение интерфейса класса Matrix #include "Matrix.h"

bool is_equa1( const Matrix &ml, const Matrix &m2 )

{

/* Если содержимое двух объектов Matrix одинаково,

*возвращаем true;

*в противном случае - false

*/

// сравним количество столбцов

if ( ml.colSize() != m2.co1Size() )

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

// сравним количество строк

if ( ml.rowSize() != m2.rowSize() )

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

//пробежимся по обеим матрицам, пока

//не найдем неравные элементы

for ( int row = 0; row < ml.rowSize(); ++row ) for ( int col = 0; co1 < ml.colSize(); ++co1 )

if ( ml[row][col] != m2[row][col] ) return false;

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

//для случая равенства

}

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

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

Matrix grow( Matrix* p ) { Matrix val;

// ...

return val;

return. Например:

}

grow() возвращает вызывающей функции копию значения, хранящегося в переменной val.

Такое поведение можно изменить, если объявить, что возвращается указатель или ссылка. При возврате ссылки вызывающая функция получает l-значение для val и потому может модифицировать val или взять ее адрес. Вот как можно объявить, что grow() возвращает ссылку: