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

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

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

280

Часть II • Объектно-ориентированное програм1М1ировоние но C-f^-

 

В разделе по передаче параметров-структур обсуждались тип Account и функ­

 

ция swapAccountsO, в которой параметр Account передавался по указателю.

 

struct Account

/ /

для упрощения

- просто два поля

 

long num;

 

double bal; }

 

 

 

 

void swapAccounts (Account *a1, Account

*a2)

/ /

нужен Account

 

{ Account temp;

 

 

 

 

i f

((*a1).num > (*a2).num)

 

 

 

 

{

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

}

 

 

Теперь рассмотрим еш,е один тип — Transaction — и попытаемся передать его функции swapAccountsO переменную этого типа. Компилятор пресекает эту попытку, фиксируя синтаксическую ошибку:

struct Transaction { long num;

double amt; } ;

Transaction tran1, tran2; ...

swapAccounts(&tranl,&tran2);

/ /

TO же имя и тот же тип, что в Account

/ /

другое имя, но такой же тип

/ /

код клиента

/ /

ошибка: неверный тип аргумента

Но ведь это, по существу, одинаковые структуры, и хотелось бы использовать для них одну функцию swapAccountsO, а не писать еще одну — swapTransactions(). Я знаю, что делаю, и хотел бы убедить компилятор принять этот код, а не генери­ ровать ошибку. СН-+ предоставляет такую возможность: явное приведение типа. Нужно лишь преобразовать указатель Transaction к типу указателя Account, после чего компилятор примет данный код:

swapAccounts((Account*)&tran1,(Account)&tran2);

/ / нет ошибки

Выглядит довольно неприглядно, но работает, подтверждая мое право на свободу действий. Другие программисты могут сказать, что я ишу приключений на свою голову. Действительно, в процессе сопровождения программы любой из типов — Account или Transaction — могут измениться, и тогда... Да поможет нам Бог. Возможно, надежнее будет все же написать небольшую функцию swapTransactions().

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

Transaction tran1, tran2; ..

/ /

объекты транзакций

printAccounts(tran1,tran2);

/ /

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

 

/ /

неверный тип аргументов

Если бы я настаивал на своем праве сделать это, то мог бы использовать приве­ дение типа Transaction к Account.

printAccounts((Account&)tran1,(Account&)tran2);

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

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

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

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

281

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

Рассмотрим, к примеру, функцию copyAccountsO, копирующую один массив типа Account в другой массив:

void

copyAccounts(Account cJest[],

const Account

s r c [ ] , int size)

{

for (int

i=0; i < size; i++)

 

 

 

clest[i]

= s r c [ i ] ; }

/ /

тот же код, что и для СоруО

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

Transaction tran1[5], tran2[5]; ...

/ /

массивы транзакций

int clata1[20], data2[20]; ...

//массивы целых

copyAccounts(tran1,tran2,5);

/ /

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

 

/ /

неверный тип аргумента

copyAccounts(data1,clata2, 20);

/ /

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

 

/ /

неверный тип аргумента

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

copyAccounts((Account*)tran1,(Account*)tran2,5);

/ /

нет ошибки

copyAccounts((Account*)clata1, (Account*)data2, 20);

/ /

нет ошибки?

Поскольку переменные типа Transaction имеют тот же размер, что и объекты типа Account, первый вызов функции может дать что-то разумное, в отличие от второго, где вместо int копируется 20 фрагментов памяти размером'Account, что приводит к порче содержимого памяти.

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

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

Возврат значения из функции

Во всех предыдущих примерах функция возвращала тип void или встроенный скалярный тип int. Конечно, функции C++ возвращают данные по значению. Следовательно, копия возвращаемого значения в области действия функции при­ сваивается переменной в области действия вызывающей программы.

C++ позволяет возвращать из функции структуру, если функция — структур­ ного типа. В приведенных ниже примерах используется модифицированная версия функции swapAccountsO. Как и в предыдущей версии, она сравнивает номера счетов, заданных двумя аргументами Account, и меняет аргументы местами, если они не упорядочены (т. е. номер счета в первом аргументе больше, чем номер счета во втором). В отличие от предыдущей версии, функция возвращает перемен­ ную Account с большим номером счета (параметр а2).

Account swapAccDunts (Account &a1, Account &a2)

/ / новый возвращаемый тип

{ Account temp = a1;

 

i f

(a1.num > a2.num)

 

{

a1 = a2; a2 = temp; }

 

return a2.num; } // плохой возвращаемый тип: нет преобразования изlong

282

Часть II • Объектно-ориентировонное програттыровамте на С-^'^

Здесь применяются правила строгого контроля типов. Если B03Bpdu;dcjvjbiii ти определяется как структура (в данном случае Account), то такой же тип должен использоваться в двух других местах — в возвращаемом выражении (в функции) и в переменной в области действия вызывающей программы. Ни выражение, ни переменная в вызывающей программе не могут быть встроенного типа, иметь другой структурный тип или массив. Преобразование между этими типами невоз­ можно:

Account ас1, ас2, асЗ; long acc_num; ...

/ /

значение в области действия

 

/ /

вызывающей программы

acc_num = swapAccounts(ac1,ac2);

/ /

ошибка: нет преобразования

Аналогично передаче параметров возвращаемые из функции значения требуют координации кода в трех местах:

1)в возвращаемом типе;

2)в возвращаемом выражении;

3)в переменной в области действия вызывающей программы. Вполне законно применение во всех этих трех местах одного и того же типа:

Account swapAccounts (Account

&a1, Account &a2)

 

 

{ Account temp = a1;

/ /

инициализация temp значением a1

i f

(a1.num > a2.num)

/ /

проверка-порядка

счетов

 

{

a1 = a2; a2 = temp; }

/ /

поменять местами,

если аргументы

return a2.}

/ /

не упорядочены

 

 

/ /

корректный тип возвращаемого

выражения

ac3 = swapAccounts(ac1,ac2);

/ /

корректный тип возвращаемого

значения

Это допустимо, но при больших структурах и при частом вызове функции будет работать медленно. Именно поэтому большинство функций возвращают тип void, целое или булево значение, которые указывают на успешное или неуспешное выполнение функции.

C++ не позволяет функции возвращать массив, но предоставляет возмож­ ность возвращать указатель или ссылку. Это можно использовать для преодо­ ления проблемы копируемого возвращаемого значения. В следующем примере функция swapAccountsO сравнивает поля num аргументов Account, меняет их местами, если нарушен порядок, и возвращает указатель на переменную Account, у которой номер счета больше:

Account*

swapAccounts

(Account &a1, Account &a2)

/ / возвращает

указатель

{ Account

temp = a1;

 

 

 

i f

(a1.

mtn>al.

num)

 

 

 

{

a1 = a2; a2 -

temp;

}

 

 

return

&a2; }

 

/ / возвращает адрес фактического

аргумента

Здесь типы также должны быть совместимы. Это касается трех мест:

1)объявленного типа, возвращаемого функцией;

2)типа выражения, возвращаемого функцией;

3)типа переменной в области действия.

Account ас1, ас2, асЗ, *ас4; ...

ас4 = swapAccounts(ac1,ac2);

// ас4 - указатель, а неAccount

ac4->num = 0;

 

// влияет нанеупомянутые ас1 или ас2

*ас4 = асЗ;

// копирование асЗ в структуру с большим номером счета

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

283

Как видно, возврат указателя позволяет применять весьма туманные способы программирования в клиентском коде типа ac4->num = 0. Это может давать ссылку на ас1 или ас2 — при сопровождении приложения придется изучать реализацию функции-сервера, например swapAccount(). Если эта функция также написана не очень понятно, понадобится обраш,аться к другим сегментам кода. Все это услож­ няет задачи сопровождения и повышает вероятность ошибок. Возврат указателей дает возможность применять в клиенте более изош,ренный синтаксис. Например, можно присвоить О счету с большим номером при помош.и строки:

swapAccount(ac1,ac2)->num = 0; / / неплохо, не правда ли?

Если нужно скопировать переменную асЗ в структуру с большим значением остатка на счете, то можно воспользоваться следуюш^им оператором:

*swapAccount(ac1,ac2) = асЗ; / / на самом деле это не очень хорошо

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

const Account swapAccounts

(Account &a1, Account &a2)

/ /

новая идея

{ Account temp = a1;

 

 

 

i f

(al.num

> a2.num)

 

 

 

{

a1 = a2;

a2 = temp; }

 

 

 

return &a2; }

/ / возврат адреса фактического

аргумента

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

*swapAccounts(ac1,ac2)=ac3;

/ /

ошибка: изменения в объекте const

 

/ /

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

swapAccounts(ac1,ac2)->num = 0;

/ /

ошибка: нельзя изменять const

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

Account *ас5 = swapAccounts(ac1,ас2);

/ /

синтаксическая ошибка поэтому

ac5->num = 0;

/ /

данный код не компилируется

Такое возвраш^аемое значение можно применять только для доступа к компо­ нентам объекта или присвоить его указателю на объект const:

const Account *ас5 = swapAccounts(ac1,ac2);

/ /

теперь OK

ac5->num = 0;

/ /

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

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

284 Часть II • Объектно-ориентированное программирование но С-^4^

Рассмотрим, например, следующую реализацию функции swapAccountO, воз­ вращающую указатель на структуру. Номер счета для этой структуры берется из параметра a1:

Account*

swapAccounts

(Account

&a1, Account &a2)

/ / возвращает указатель

{ Account

temp = a1;

/ /

temp содержит данные из a1

i f

(al.num > a2.num)

 

 

 

{

a1=a2; a2=temp; }

/ /

a1 может измениться,

но temp содержит его данные

return &temp; }

/ /

так чей это адрес?

 

Когда достигается закрывающая фигурная скобка области действия функции, переменная temp уничтожается. Следовательно, указатель ас4 в клиентском коде ссылается не на адрес структуры, содержащей данные из переменной ас1, а на память, уже не принадлежащую программе. Это называется "повисшим" указа­ телем. Такой указатель ссылается на уже исчезнувший объект.

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

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

Возвращать указатели на локальные переменные небезопасно. Более надежно возвращать указатели на переменные в динамически распределяемой области памяти или на переменные в области действия клиента. Вот пример возврата указателя на переменные в области действия^клиента, когда решается проблема "повисшего" указателя:

Account* swapAccounts (Account

&a1, Account &a2)

/ / возвращает указатель

{ Account^^femp = a1;

/ /

temp содержит данные из a1

i f

(al/num > a2. num)

 

 

 

{

a1=a2;

a2=temp;

 

 

 

 

return

&a2; }

/ /

данные из a1 теперь в a2

return &a1; }

/ /

данные из a1 теперь снова в a1

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

Account*

largerBalance (const Account

&a1, const Account &a2)*

/ / нет!

{ return

(a1. bal>a2. bal) ? &a1 : &a21

}

/ / указатель на фактический аргумент

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

const Account

асс1 = {325,1000.0}, асс2 = {370,100.0};

/ / не изменяется

Account *р = largerBalace(acc1,acc2);

/ /

допустимый,

но опасный синтаксис

p->bal = 0;

/ / допустимый синтаксис,

но модифицирует объект-константу

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

285

Это кажется избыточной мерой. Хотя параметры функции 1агдегВа1апсе() определяются как константы, они могут передавать отличные OTConst аргументы:

Account

асс1 = {325,1000.0}; асс2 = {370,100.0};

/ / объект изменяется

Account

*р = 1агдегВа1асе(асс1, асс2);

/ / допустимый,

но опасный синтаксис

p->bal

= 0;

/ / OK для не константы,

но не годится для объекта-константы

Однако компилятор не настолько интеллектуален, чтобы различать функции между передаваемыми объектами-константами и не константами. Как иногда бы­ вает в реальной жизни, решение состоит в том, чтобы просто забыть обо всем остальном, поэтому C++ требует указывать в возвращаемом типе модификатор const (обратите внимание, что я не предлагаю y6paTbConst из заголовка функции)

const Account* largerBalance (const Account

&a1, const Account

&a2)

{ return (a1.bal>a2.bal) ? &a1 : &a2; }

/ / такой код

компилируется

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

const Account acci = {325,1000.0}, асс2 = {370,100.0};

/ / не изменяется

Account

*р = largerBalance(acc1,acc2);

/ /

теперь это синтаксическая ошибка

p->bal

= 0;

/ /

нет синтаксической ошибки, но

 

 

/ /

компилятор такого не разрешает

Компилятор хочет предотвратить присваивание вида p->bal = 0: операция син­ таксически корректна, но он помечает присваивание указателю р, поскольку это не гарантирует от модификации указываемого объекта. Компилятор заставляет программиста координировать свои действия и определить р как указатель на объект-константу.

const

Account

acci = {325,1000.0}, асс2 = {370,100.0};

/ /

не изменяется

const

Account

*р = largerBalance(acc1,acc2);

 

/ /

OK,

нет ошибки

p->bal = 0;

/ /

нет синтаксической

ошибки, но

 

 

/ /

компилятор такого не разрешает

Когда объекты Account не определяются как неизменяемые, это будет избы­ точным — указатель р нельзя использовать для их модификации. Данная избыточ­ ность — плата за безопасность объектов-констант (и за нежелание разработчиков компилятора анализировать поток данных в программе).

Самый безопасный способ использования адресов для возврата значений — это динамическое управление памятью. Функция-сервер распределяет динамиче­ скую память и возвращает указатель на эту память клиенту. (Некоторые другие функции должны позднее освобождать данную память.) В нашем примере функция allocateAccountsO распределяет динамический массив^объектов Account. Размер массива передается как аргумент.

Account* allocateAccounts(int size)

/ /

указатель на не константу

{ i f

(size

<= 0) return 0;

/ /

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

Account

*р = new Account[size];

 

 

i f

(р == 0)

/ /

просто, но слишком "сыро"

 

cout

«

"В allocateAccountsO

нет памяти\п";

return

р;

}

/ /

NULL, если что-то не так

Если что-то не так, функция возвращает указатель NULL. Клиент должен прове­ рять, успешно ли выделена память.

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

286

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

Концептуально она аналогична возврату указателя на структуру, но с практиче­ ской точки зрения весьма отличается, поскольку в C++ по умолчанию ссылки являются константами. Рассмотрим версию функции swapAccountsO, которая возвращает ссылку на фактический аргумент с большим номером счета.

Account& swapAccounts (Account

&a1, Account &a2)

{ Account temp = a1;

 

i f

(al.num > a2.num)

 

{

a1=a2; a2=temp; }

 

return a2; }

/ / неверный тип, если возвращается &а2

Обратите внимание, что эта версия аналогична версии, возвращающей струк­ туру по значению. Единственная разница в операции & в возвращаемом типе. Было бы некорректно возвращать &а2 вместо а2, так как а2 имеет тип Account и может использоваться для инициализации ссылки типа Account, а &а2 — указа­ тель типа Account и не может применяться для инициализации ссылки Account. Эти два типа в C++ несовместимы.

При этом в клиентском коде могут легко возникнуть проблемы:

Account ас1,ас2,аЗ, &ас4; ...

/ /

здесь это ссылка

ас4 = swapAcccounts(ac1,ac2);

/ /

фантазии, а не реальный код

Этот фрагмент некорректен. Здесь возвращаемое функцией swapAccountsO значение присваивается переменной ас4, но слишком поздно. Ссылки должны инициализироваться при определении, а выше этого не сделано. Единственный способ присвоить такое возвращаемое значение — использовать его для инициа­ лизации:

Account

ас1,ас2,аЗ;...

 

 

 

Account

&ас4 = swapAcccounts(ac1, ас2);

/ / на этот раз ОК

ac4.num = 0;

/ /

влияет на не упомянутые здесь ас1 или ас2

ас4 = аЗ;

/ /

копирование аЗ в структуру

с большим номером счета

Поскольку ас4 — синоним ас1 или ас2, результат данного кода не вполне ясен. Так как возвращаемое функцией swapAccountsO значение представляет собой ссылку, можно использовать для возврата указателя вот такой хитрый синтаксис:

largerBalance(ac1,ac2).num = 0;

/ /

прекрасно, не правда ли?

largerBalance(ac1,ac2) = аЗ;

/ /

это тоже неплохо

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

вподобных вычислениях.

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

асЗ = largerBalance(ac1,ac2);

/ / асЗ не ссылка, а переменная типа Account

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

Данная тема достаточно сложна и требует учета множества других моментов. Чтение и понимание программного кода, где возвращаются структурные значения, указатели и ссылки — задача нетривиальная. Стоит ли прибегать к этому ради удобства или производительности? Существуют ли другие, более простые способы получения того же результата?

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

287

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

Встраиваемые функции

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

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

double tax(double gross) { return gross * 0.05; }

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

double sales, state; ...

 

state = tax(sales);

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

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

#define tax(x) х*0.05

Клиентский код state = tax(sales); расширяется в state = sales*0.05; — так удается избежать непроизводительных потерь при вызове функции.

C++ поддерживает такие же возможности макрокоманд, как и язык С, однако расширение макрокоманд выполняется препроцессором, а не транслируется ком­ пилятором. Макрокоманда — не функция. Она не может иметь локальных пере­ менных, не предусматривает проверки типа параметров и невидима для отладчика.

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

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

state = tax(sales+20.2);

/ / выражение как фактический аргумент

Для программиста это означает:

 

state = (sales+20.2) * 0.05;

/ / желательная интерпретация

t 288 I

Часть I! # Объвктио-ортештг

oe орограгл : .. хштв на C++

 

Но препроцессор вычисляет выражение, используя литеральную подстановку

 

макрокода:

 

 

state = sales+20.2 * 0.05;

/ / интерпретация препроцессора

Конечно, с данной проблемой можно справиться (например, включив в макро­ определение скобки), но пример демонстрирует, что макрокомандам свойственны недостатки, которых лучше избегать. C++ позволяет программисту объявить функцию как встраиваемую (подставляемую). Это средство подобно макрокоман­ де, но не имеет недостатков оператора препроцессора #clef ine.

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

inline double

tax(double gross)

{ return gross

* 0.05; }

В TO же время функция inline — действительно функция. Она может содержать несколько строк, определять вложенные блоки и иметь локальные переменные. Как и любая функция C+ + , функция inline допускает контроль типов парамет­ ров и операции отладки.

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

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

Модификатор inline не является для компилятора безусловной командой. Это лишь предложение. Если функция слишком сложная или слишком длинная, ком­ пилятор может игнорировать его (таково мнение разработчиков компилятора).

Некоторые компиляторы не воспринимают в функциях inline никаких управ­ ляющих конструкций. Другие допускают один или два оператора if, но игнорируют функции, содержащие циклы. Используйте данное средство только для простых функций.

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

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

struct Counter {

 

private:

 

int cnt;

 

public:

 

void InitCnt(int Value)

// inline noумолчанию

{ cnt = Value; }

void UpCntO

 

{ cnt ++; }

 

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

289

void DnCntO { cnt-; } int GetCntO

{ return cnt; } } ;

В спецификации класса обычно задаются только прототипы функций, а не их реализация.

struct Counter {

 

private:

 

int cnt;

 

public:

// только прототип

void InitCnt(int);

void UpCntO;

 

void DnCntO

 

int GetCntO } ;

 

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

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

void Counter::InitCnt(int Value) { cnt = Value; }

inline void Counter::UpCnt()

{ cnt++; }

inline void Counter::DnCnt() { cnt-; }

inline int Counter::GetCnt()

{return cnt; }

Оклассах подробнее рассказывается в следующих главах.

Параметры с заданными по умолчанию значениями

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

Ниже приведено объявление функции sum(), вычисляющей сумму заданного числа компонентов массива значений double. В этом объявлении используется синтаксис инициализации второго формального параметра. Для него по умолча­ нию задается значение 25:

double sum (const double a [ ] , int n=25); / / прототип

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

double

t o t a l ; double х[100]; int n;

...

/ /

и т. д.

t o t a l

= sum(x);

/ /

складывает

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

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

t o t a l = sum(x,n);

/ / складывает п компонентов массива

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