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

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
267
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

I 270

Часть II • Объектно-ориентированное riporpaMtf^HF^BaHHe но С-^^

Перед обменом значениями: a1=82 а2=42

После

обмена значениями:

a1=42 а2=82

После

вызова:

х=42 у=82

Рис. 7 . 4 . Вывод для программы из листинга 7.4

аргументов в программе-клиенте. Возможно, ключевое сло­ во проще и легче запоминается, чем операция.

Давайте теперь реализуем функцию перестановки значе­ ний аргументов с помоидью передачи по ссылке. Исходный код показан в листинге 7.4. Изменения достаточно просты. Исходный код также стал намного понятнее, чем при применении указателей. Уменьилилась вероятность ошибок. Результаты представлены на рис. 7.4.

Листинг 7.4. Передача параметров по ссылке (надежный метод)

 

#inclucle

<iostream>

 

 

 

 

 

 

 

using namespace std;

 

 

 

 

 

 

 

void

swap (int

&a1, int &a2)

 

 

/ /

корректный режим передачи

параметров

{ int

temp;

 

 

 

 

 

 

 

 

i f

(a1 > a2)

{

 

 

/ /

разыменования не требуется

cout «

"Перед обменом значениями: a1=" «

a1 «

" а2=" «

а2 «

endl;

 

temp = a1; a1 = a2; a2 = temp;

 

a1 «

" a2=" «

a2 «

endl;}}

 

cout « "После обмена значениями: a1=" «

 

int mainO

 

 

 

// неупорядоченные значения

 

{ int X = 82, у = 42

 

 

 

// { int X = 42, у = 84;

 

 

// упорядоченные значения

 

swap(x,y);

 

" у=" «

у «

// верный режим передачи параметров

cout «

"После вызова: х=" « х «

endl;

 

 

 

return 0;

 

 

 

 

 

 

 

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

В таблице 7.1 отображены правила передачи параметров. Здесь термин var

 

 

 

(с операциями, там, где это применимо) обозначает имя переменной, используе­

 

 

 

мой в качестве аргумента в вызове функции, параметра в заголовке функции

 

 

 

(и прототипе) и в теле функции.

 

 

 

 

 

 

 

 

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

 

 

 

или к параметру в теле функции не применяются операции. Передача по указате­

 

 

 

лю более сложна, так как в трех местах нужны операции: в аргументе, в параметре

 

 

 

и в теле функции. Передача по ссылке аналогична передаче по значению. Един­

 

 

 

ственная разница — операция ссылки в заголовке функции.

 

 

 

 

При передаче по ссылке нет свойственной передаче по указателю сложности,

 

 

 

но поддерживается изменение аргументов в области действия клиента. Если пара­

 

 

 

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

 

 

 

всегда будет понятно, что разработчик хотел сохранить значения фактических

 

 

 

аргументов.

 

 

 

 

 

 

 

 

 

 

Передача параметров в случае простой переменной

Таблица 7.1

 

 

 

 

 

 

 

Элемент

 

 

 

 

 

 

 

 

 

 

исходного кода

По значению

 

По указателю

По ссылке

 

 

 

Вызов функции

var

 

 

&var

 

var

 

 

 

Заголовок функции

var

 

 

*var

 

&var

 

 

 

Тело функции

var

 

 

*var

 

var

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

Глава 7 • Программирование с использованием функций C-f-i-

271

1-значения можно использовать только как фактические аргументы, они должны быть одного типа, поскольку C++ не поддерживает неявного преобразования ссылок на разные типы. Явное преобразование возможно, но бесполезно, так как переменная-ссылка позволяет обращаться лишь к значению того типа, которое было задано в определении. Применение выражений, литералов и констант не допускается.

С о в е т у е м Когда функция не изменяет значений своих параметров встроенных типов C++, передавайте параметры по значению.

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

Структуры

Структуры (и объекты классов) можно передавать по значению, по указателю или по ссылке. Если переменная-структура используется функцией "на входе" и не изменяется в ее теле, ее можно передавать по значению. Если структура является выходным значением функции (т. е. служит для передачи значений функции-клиенту), и в теле функции модифицируются ее поля, то следует переда­ вать ее по указателю или по ссылке. В противном случае эти изменения не будут действовать в пространстве клиента.

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

Для простоты в качестве примера рассмотрим упрощенный тип Account:

struct

Account {

long

num;

/ / для простоты только два поля

double bal;

} ;

Функция доступа к этой структуре printAccounts() имеет два параметра-пере- менных типа Account и выводит два значения: номер счета и остаток на счете. Эти объекты Account являются для printAccountsO исходными переменными. Их значения должны устанавливаться в клиентском коде перед вызовом, так как функции printAccountsO для правильной работы нужны уже установленные значения переменных типа Account:

void printAccounts(Account a1, Account

a2)

/ / код сервера

{ cout « "Номер первого счета: No. " «

al.num

 

«"Остаток: " « al.bal « endl;

cout «

"Номер второго счета:

No.

" «

a2.num

«

"Остаток: " « a2.bal

«

endl;

}

В клиентском коде создаются объекты Account, инициализируются их поля и вызывается серверная функция printAccountsO для вывода состояния счетов:

Account X, у;

 

/ / код клиента

x.num = 800123456;

x.bal

= 1200;

у.num = 800123123;

у.bal

= 1500;

printAccounts(x,y);

 

 

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

272

I

Чость II * Объектно-ориентирОБ

 

 

Здесь действуют базовые правила передачи параметров, т. е. программисту

 

 

нужно координировать код в трех разных местах: в вызове функции, в заголовке

 

 

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

 

 

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

 

 

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

 

 

же операция-точка (селектор), что и в клиентском коде. Это простейший режим

 

 

передачи параметров. (Сравните обозначение al.bal в функции printAccountsO

 

 

и X. bal в клиенте.)

 

 

Теперь рассмотрим другую функцию доступа — swapAccountsO, которая срав­

 

 

нивает номера счетов в своих параметрах и меняет параметры местами, если но­

 

 

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

 

 

должны изменяться, передача параметров по значению уже не подходит. Эта

функция передает свои параметры по указателю:

void swapAccounts(Account

*a1, Account

*a2)

/ /

передача по указателю

{ Account temp;

 

 

 

 

i f

(a1->num > a2->nufn)

*a2 = temp; }

 

/ /

операция

{

temp = *a1, *a1 = *a2;

}

 

 

Когда клиентский код вызывает эту функцию, он передает ей адреса фактических аргументов.

swapAccounts(&x,&y);

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

Это общее правило не ограничивается передачей параметров. Операция точки позволяет выбрать поле, когда левый операнд задает имя переменной-структуры. Операция-стрелка применяется в том случае, когда ее левым операндом является указатель на структурную переменную. Не путайте эти две операции. Для про­ граммистов различие часто очевидно. Когда используется указатель, не важно, на что он ссылается — на именованную переменную в стеке или на неименованную переменную в динамически распределяемой области. Для указателя нужна опера­ ция-стрелка. Если одна операция применяется в контексте, где требуется другая операция, генерируется сообщение об ошибке (иногда достаточно непонятное).

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

void swapAccounts(Account

* ptrA1, Account *ptrA2)

/ /

передача по указателю

{ Account temp;

 

 

 

 

i f

(ptrA1->num > ptrA2->num)

/ /

операция

{

temp = * ptrA1, * ptrA1

= * ptrA2; * ptrA2 = temp;

}

}

Ha мой взгляд, предыдущая версия лучше, так как имена параметров (типа Account) вида *a1 и *а2 привычнее (для меня они начинаются со звездочки), а компонент ptr в имени малопривлекателен. Тем не менее подобная практика общепринята, и можно следовать ей при работе с указателями.

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

Глова 7 • Программирование с использованием функций С++

273

void swapAccounts (Account *a1, Account

*a2)

// передача noуказателю

{ Account temp;

> (*a2).num)

 

// нет селектора-стрелки

if ((*a1).num

}

{ temp = *a1;

*a1 = *a2; *a2 = temp; }

 

 

Круглые скобки здесь важны, так как операция-селектор имеет более высокий приоритет, чем разыменование. Выражение без скобок, например, *a1.num будет понято компилятором не как (*a1).num, а как *(a1.bal), что не имеет смысла. Во-первых, выражение *a1.bal незаконно, так как указатель a1 не может рабо­ тать с селектором-точкой. Во-вторых, даже если бы выражение a1. bal было до­ пустимым, поле bal имеет тип double, а разыменовать значение double, понятно, нельзя — это должен быть указатель.

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

В функции swapAccountsO структуры передаются по указателю, а не по значе­ нию, поскольку фактические аргументы должны изменяться в результате переста­ новки. Некоторые программисты, особенно, имеюш^ие солидный опыт работы на языке С, не любят передавать структуры по значению и передают их по указателю, даже когда исходные переменные в процессе выполнения функции не изменяются. Вот как могла бы выглядеть функция printAccountsO, если бы ее написал такой программист. Параметры здесь передаются по указателю, а не по значению, а для обраш^ения к полям структуры используется селектор-стрелка:

void printAccounts(Account *a1, Account

*a2)

/ / вводит в заблуждение

{ cout « "Номер первого счета: No. " «

a1->num

 

«"Остаток: " « a1->bal « endl;

cout « "Номер второго счета: No. " « a2->num

«"Остаток: " « a2->bal « endl; }

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

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

Более суш,ественный вопрос — понятность намерений разработчика. Разра­ ботчик не хочет изменять аргументы функции printAccountsO. Между тем, он скрывает это знание от программиста, сопровождаюш,его приложение, так как в заголовке функции ясно говорится, что возможны изменения, ведь.параметры передаются по указателю. То же самое получается, когда в вызове функции аргу­ менты передаются по указателю, поскольку здесь используется операция получе­ ния адреса:

printAccounts(&x,&y);

/ / ясно, аргументы можно изменять!

274Часть II • Объектно-ориентированное программирование на C++

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

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

Перед перестановкой

800123456

Остаток:

1200

Номер

первого счета:

Номер

второго счета:

800123123

Остаток:

1500

После

перестановки:

800123123

Остаток:

1500

Номер

первого счета:

Номер

второго счета:

800123456

Остаток:

1200

Рис. 7.5. Вывод программы из листинга /.о

ности при передаче по значению. При этом четко видно намерение разработчика.

Как же быть? В листинге 7.5 приведен пример программы, реализующей обе серверные функ­ ции — printAccountsO и swapAccountsO — пу­ тем передачи параметров по ссылке. Результаты программы представлены на рис. 7.5,

Листинг 7.5. Пример передачи параметров по ссылке

#inclucle <iostream> using namespace std;

struct Account { long num; double bal; } ;

void printAccounts(cons

Accounts &a1, const Account &a2)

/ /

заголовок

{ cout

«

"Номер первого счета: No. " «

al.num

/ /

тело

 

«

"Остаток: " «

al.bal

«

endl;

 

 

 

cout

«

"Номер второго

счета:

No.

" «

a2.num

 

 

«"Остаток: " « a2.bal « endl; }

void swapAccounts(Account

&a1, Account &a2)

/ /

заголовок

{ Account

temp;

 

 

/ /

тело

i f

(al.num > a2.num)

 

 

 

{

temp = a1; a2 = a2; a2 = temp; } }

 

 

int

mainO

 

 

 

 

{

 

 

 

 

 

 

Account

X, y;

 

 

 

 

x.num = 800123456;

x.bal

= 1200;

 

 

y.num = 800123123;

y.bal

= 1500;

 

 

cout «

"Перед перестановкой\п";

 

 

printAccounts(x,y);

 

 

/ /

вызов

swapAccounts(x,y);

 

 

/ /

вызов

cout «

"После перестановки\п";

 

 

printAccounts(x,y);

 

 

 

 

return

0;

 

 

 

 

}

 

 

 

 

 

 

Функция swapAccountsO достаточно проста. В вызове функции используется имя структуры, обозначение ссылки применяется в заголовке функции, и имя структуры — в ее теле. Для доступа к полям в теле функции служит операцияточка, что опять поднимает вопрос выбора корректного селектора. Как видно, при передаче параметров-структур по ссылке обозначения проще, чем при пере­ даче по указателю.

Глава 7 • Програ1У11^1ирование с использованием функций C-I-+

CJZO

 

Функция printAccountsO также проста. В вызове функции указывается имя структурной переменной (как при передаче по значению), а в теле функции ис­ пользуются имена параметров. Доступ к полям структуры осуществляется точно так же, как при передаче по значению. Все различия — в заголовке функции: здесь с именами параметров используется обозначение ссылки и ключевое слово const перед типами параметров. Первое исключает копирование фактических аргументов (функции передаются их адреса, а не копии полей), а второе предот­ вращает изменение значений параметров в функции и ясно говорит о том, что параметры не изменяются. Нет необходимости проверять тело функции, чтобы убедиться в этом.

Применение ключевого слова (модификатора) const аналогично его исполь­ зованию в определении переменных. Данный модификатор можно применять со значениями, указателями и ссылками. Когда он указывается со значениями, то го­ ворит о том, что значение не может изменяться ни путем прямого присваивания, ни через указатель или ссылку.

cosnt int val

= 10;

/ / инициализация обязательна

val

= 20;

/ /

синтаксическая ошибка: присваивание не допускается

int

*р = &val;

/ /

не допускается

для запрета

косвенного изменения *р = 20;

int

&г = val;

/ /

не допускается

для запрета

косвенного изменения г = 20;

Когда модификатор const используется с указателями и ссылками, у него в зави­ симости от позиции может быть два смысла. Если он указывается перед именем типа, это значит, что значение не может модифицироваться путем разыменования указателя или через ссылку:

const

int

val = 10;

 

 

const

int

*constp

= &val;

/ /

OK, но *constp=20 даст синтаксическую ошибку

const

int

&constp

= val;

/ /

OK, H0'constr=20 даст синтаксическую ошибку

Обратите внимание, что непосредственное изменение переменной val, например val = 20, не допускается, поскольку данная переменная определена как константа. Косвенная модификация также недопустима, но не потому, что "переменная" val — константа, а потому, что переменная-указатель (и ссылка) определена как указа­ тель на const. Для указателя (или ссылки) на константу косвенная модификация не допускается, даже при ссылке на "не константу". Почувствуйте разницу:

int value

= 10;

 

/ /

эта переменная может изменяться

const

int

*constp

= &value;

/ /

*constp=20 все равно даст

 

 

 

 

/ /

синтаксическую ошибку

const

int

&constr

= value;

/ /

*constr=20 все равно даст

 

 

 

 

/ /

синтаксическую ошибку

Когда модификатор const находится между именем типа и именем указателя, это означает, что указатель является константой. К нему можно применить разы­ менование, а значение, на которое указатель ссылается, можно изменить, однако не допускается присваивание указателю другого адреса. Хотя инициализация не обязательна, она необходима. Если указатель-константу не инициализировать при определении, позднее ему уже ничего нельзя будет присвоить:

int

value

= 10;

/ /

в данном примере это не константа

i n t *

const

pconst = &value;

/ /

теперь навеки

вместе

*pconst

= 20;

/ /

OK, значение

-

не константа

pconst

= NULL;

/ /

синтаксическая ошибка:

 

 

 

 

/ /

указатель -

константа

Ссылочную константу объявить нельзя, так как ссылки в C++ являются кон­ стантами по умолчанию. Они инициализируются в определении и не могут указы­ вать на другой адрес. Используемое в передаче параметров обозначение говорит

276

Часть li * Объ&кто-ортештровониое програтттроваитв на C^-i-

 

0 том, что ссылку не только нельзя переустановить на другой адрес, но и этот адрес

 

не может изменять своего значения. Данная ссылка инициализируется значением

 

фактического аргумента во время вызова функции.

 

Что происходит, если разработчик устает анализировать все эти тонкости

 

и передает структуру по ссылке без модификатора const? Ничего особенного. Вот

упрощенная функция printAccounts():

void printAccounts(Account &a1, Account &a2)

/ / можно изменять?

{ cout

«

"Номер первого счета: " «

a1->num

 

 

«

"Остаток: " «

a1->bal

«

endl;

 

cout

«

"Номер второго счета:

" «

a2->num

 

 

«

"Остаток: " «

a2->bal

«

endl; }

 

Корректная ли эта функция? Да. К тому же, обещая не изменять свои параметры, она их не изменяет. Тем не менее это несомненный вклад в кризис ПО. Разра­ ботчик не сообщил, что у него не было намерений модифицировать параметры в теле функции.

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

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

Аналогично, отсутствие модификатора const должно ясно говорить сопровож­ дающему приложение программисту, что параметры меняются в теле функции, а не о том, что разработчик рассеян и забывчив. Звучит довольно витиевато, но в C + + нет другого способа различать входные и выходные параметры.

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

обозначьте это тем, что const не используется.

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

Массивы

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

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

Глава 7 • Прогромг^рировоние с испоАШОвантет функций C++

| 277 |

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

void

Copy(double

dest[], double s r c [ ] , int

size)

{

for (int i=0;

i < size; i++)

/ / классический цикл

 

d e s t [ i ] = s r c [ i ] ; }

 

Конечно, вызывающая сторона должна обеспечить достаточно компонентов для данной операции. СН-+ не дает программисту защиты от порчи содержимого памяти. Вот пример клиентского кода:

double х[100], у[100]; int п-0;

 

 

do {

 

 

 

 

 

cout

«

"Введите данные

(О для завершения): ";

 

cin

»

у[п++];

/ /

заполнение

массива у [ ] , присваивание п

. . .

} while (true);

 

 

 

Copy(x,y,n);

/ /

копирование

n компонентов у [ ] в х [ ]

В данном примере показано, что при передаче массивов задается:

Имя массива без квадратных скобок в вызове функции

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

Компоненты массива (или имя массива без квадратных скобок)

втеле функции

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

void

Copy(double

*dest, double *src, int

size)

{

for (int i=0

i

< size; i++)

/ / классический цикл для массива

 

dest+i = src+i;

}

 

Можно также разыменовывать адреса компонентов массива, что подобно (но не идентично) передаче параметров по указателю.

void

Copy(double *dest, double *src, int

size)

 

{

for (int

i=0 i < size;

i++)

/ /

классический цикл для массива

 

*(dest+i)

= *(src+i);

}

/ /

или *dest++ = *src++;

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

double х[100], у[100]; int п;

/ /

заполнение массива у [ ] , присваивание п

Сору(&х[0], &у[0], п);

/ /

копирование п компонентов массива у[] в х[]

Тем не менее это адрес первого компонента массива, а не адрес фактического аргумента.

Какой бы синтаксис ни использовался, ни вызов, ни заголовок функции не могут указать на роль параметров (входные они или выходные). В данном примере массив scr[] является входным параметром (исходный массив). Значения его компонентов используются в теле функции для выполнения операций, а не явля­ ются результатом вызова. Массив dest[] — выходной параметр (результирую­ щий массив). Какие бы значения ни содержали его компоненты перед вызовом,

I 278

Часть II« Объектно-ориентированное програ1М1^^и[ровоние на СНИФ

в результате вызова они будут изменены. Обозначение обоих массивов во всех трех местах (вызове функции, в ее заголовке и в теле) одинаково. Это неправиль­ но. Когда разработчик не планирует изменять массив в функции, лучше использо­ вать модификатор const, чтобы сообщить об этих намерениях:

void

Copy(double

dest[], const double

s r c [ ] ,

int size)

{

for (int

i=0;

i < size;

i++)

 

 

 

d e s t [ i ]

= s r c [ i ] ; }

/ /

s r c [ i ]

= d e s t [ i ] ; синтаксическая ошибка

Как и при передаче параметров-структур, очень важно использовать при пере­ даче параметров-массивов, не изменяемых в теле функции, модификатор const, и делать это постоянно. Тем самым предотвращается изменение в функции исход­ ных переменных (ввод) и, что еще важнее, ясно видны намерения разработчика.

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

const double

с [ ] = {

1.1. 1.2, 1.3,

1.3

} ;

 

Сору(х,с,4);

Сору(у,а,4);

/ /

ОК, с [ ]

и src[] - массивы const

Сору(с,х,4);

/ /

синтаксическая

ошибка: с [ ] -

массив const, а dest[] - нет

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

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

double sum (double

а [ ] ,

int n)

{ double t o t a l

= 0.0;

 

for (int i = 0; i

< n;

i++)

t o t a l += a [ i ] ;

 

 

return t o t a l ;

}

 

 

/ /

инициализировать

подсчет

/ /

еще один классический цикл

/ /

накопление суммы

 

Позднее мне понадобилось подсчитать среднее по действительным элементам массива. Для этого нужно вычислить сумму и разделить ее на число элементов. Нетрудно было бы сделать все сначала, но, поскольку функция sum() уже есть, я решил ее использовать:

double avg

(const double

a [ ] ,

int n)

{ return

sum(a,n)/n);

}

/ / синтаксическая ошибка

Это пример передачи параметра через другую серверную функцию sum(). Преды­ дущие примеры показывают использование элементов параметра-массива и ин­ декса в квадратных скобках. Данный пример демонстрирует, как использовать имя массива в теле функции без квадратных скобок.

Основная идея примера в том, что компилятору такой подход не нравится. Вот логика компилятора: массив а[] объявлен как const в заголовке функции avg(), следовательно, он не будет модифицироваться внутри avg(). Однако в теле avg() массив а[] передается функции sumO как аргумент, эта функция не принимает на себя никаких обязательств и вполне может изменить массив, нарушая то, что прописано в функции agv().

Компилятор не проверяет, изменяет ли функция sum() массив на самом деле. Но лучше перестраховаться, поэтому компилятор пометит функцию sum() как синтаксическую ошибку.

Глава 7 • Программирование с использованием функций C++

279

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

Чтобы исправить ситуацию, я определил все параметры как const. Намерения программы должны отражаться в интерфейсе функции-сервера:

double

sum (const

double a [ ] , int n)

/ / массив на входе

{ double t o t a l

= 0.0;

 

for

(int i=0;

i<n; i++)

 

 

t o t a l += a [ i ] ;

 

return t o t a l ;

}

 

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

double sum (const double a [ ] , int n);

/ / прототип функции

Имена параметров в прототипе не обязательны. Как это работает для массивов? Выглядит все забавно, но компилятор понимает такую запись. Пусть и вас она не смуш,ает:

double sum (const double[], i n t ) ;

/ / имена параметров не обязательны

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

double sum (const double*, i n t ) ;

/ / указываемое значение не изменяется

С о в е т у е м в C++ это единственный способ передачи массивов как параметров. ЧР Чтобы различать входные и выходные массивы, используйте для входных

массивов, не изменяемых функцией, модификатор const.

Передача массивов как параметров — эффективный механизм С+Н-. Она не предполагает копирования данных, экономит память в стеке и время выполнения. Подобно передаче параметров-структур, отсутствие модификатора const должно свидетельствовать, что компоненты массива изменяются в функции. Это будет ваш вклад в борьбу с кризисом ПО.

Еще о преобразовании типов

Как уже упоминалось в разделе по преобразованию типов, в 04-4- реализован строгий подход: если в параметре ожидается скалярное значение, то структура или массив в качестве фактического аргумента использоваться не могут.

В отношении структур это правило расширено: если структура (или класс) представляет конкретный тип, ожидаемый в параметре, то скалярное значение, массив или структура другого типа в фактическом аргументе использоваться не могут — возникнет синтаксическая ошибка. Даже если структура другого типа имеет в точности тот же состав, что и ожидаемый тип, это не поможет. Когда поля обоих типов имеют одинаковый порядок, одни и те же типы и имена, компилятор все равно ждет аргумента именно того типа, имя которого совпадает с именем типа формального параметра, и больше ничего не анализирует.

Все сказанное относится к передаче структур по значению. Передача структур по указателю и по ссылке несколько отличается от передачи массивов.

Соседние файлы в предмете Программирование на C++