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

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

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

260

Часть !! * Объектно-ориентированное програтшыроваиив на С^-Ф

Как сообщить компилятору, что несоответствие типов — нормальная ситуация и вы знаете, что делаете? Обычно для этого применяется явное приведение типа, преобразующее значение одного типа в значение другого.

PutValues((int)x,(int)y);

/ /

явное приведение - для компилятора

 

/ /

и сопровождающего программиста

Вот еще один способ — приведение типа в стиле функции:

PutValues(int(x),int(y));

/ /

альтернативный синтаксис

 

/ /

для явного приведения типа

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

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

Эти преобразования аргументов аналогичны преобразованиям типов в выра­ жениях С-Ы-. Цель состоит в том, чтобы сделать операции по возможности "законными". В этом плане C + + "мягче", чем другие современные языки. Более того, применение наследования, конструкторов и перегруженных операций пре­ образования (о которых рассказывается далее) делает C + + еще менее строгим в отношении преобразования типов аргументов. Это замечательно, если такие преобразования действительно соответствуют тому, что задумывалось разработ­ чиком. Хуже, если разработчик делает ошибку, и компилятор сообщает ему об этом. Но в любом случае, применение неявного преобразования типов существенно осложняет жизнь программисту, сопровождающему программу.

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

Передача параметров в C+ +

В C + + предусмотрено три режима передачи параметров: по значению, по ука­ зателю и по ссылке.

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

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

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

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

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

261

i nt п = 22. cnt = 20; PutValues(n,cnt); PutValues(2*n,cnt-11); PutValues(18,14);

/ /

аргументы

как

переменные

/ /

аргументы

как

выражения

/ /

аргументы

как литеральные значения

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

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

int add

(int

х, int у)

/ /

х, у создаются и инициализируются

{ return

х+у;

}

/ /

у и х уничтожаются

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

int add

(int

х, int

у)

 

{ X = х+у;

 

/ /

допустимо - изменяется х

return

х;

*

/ /

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

}

 

 

/ /

модифицированная копия аргумента уничтожается

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

int а= 2, b = 3,

с; ...

с = add(a,b);

/ / переменная 'а' не изменяется в области действия клиента

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

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

с = add(2*5,b); / / передача как г-значения 2*5 функции

Для побочных эффектов C + + предусматривает передачу параметров по ука­ зателю и по ссылке. Такие режимы более сложны, чем передача параметров по значению. Некорректное их использование затрудняет сопровождение программы, а корректное — способствует ее высокой производительности и читабельности.

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

Вызов по указателю

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

Указатели — мощный и гибкий инструмент программирования, но вместе с тем и опасный. Именно по этой причине в C + + использование указателей огра­ ничивается. При определении указателя программист берет на себя некоторые обязательства: его указатель должен ссылаться на переменные типа int, double, Account, Square и пр., что не отличается от определения переменных других видов.

Во всех других отношениях указатели — это обычные переменные. Они имеют тип, могут инициализироваться, им присваиваются имена, к ним применяются операции. В конечном счете, переменные-указатели уничтожаются согласно правилам C + + . При определении переменной указателя к ней добавляется звездочка (*). Тем самым помечается, что это именно указатель. При создании указателя он не содержит допустимого значения. Как и любую другую перемен­ ную, указатель нужно инициализировать или присваивать ему значение:

i nt

v1, v2;

/ /

две целочисленные переменные;

 

 

/ /

пока что содержат "мусор"

int

*р1, *р2, *рЗ;

/ /

указатели на целые; пока никуда не ссылаются

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

NULL

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

(допускается его увеличение или уменьиление на целое число)

• Адрес переменной соответствующего типа

(оператор получения адреса С+Н

& — присоединяется слева

к имени переменой, адрес которой присвоен указателю)

v1 = 123; v2 = 456;

/ /

переменным присваивается целое значение

р1 = &v1;

/ /

указателю присваивается адрес переменной v1

р2 = р1;

/ /

указателю присваивается значение другого указателя

рЗ = NULL;

/ /

указатель на присвоенное значение NULL

Символьная константа NULL определена в заголовочном файле stdlib.h и во многих других библиотечных файлах, включая iostream. h. Это то же самое, что 0. Некоторые программисты предпочитают использовать NULL, поскольку тем самым явно указывается, что программа имеет дело с указателями. Другие программисты применяют О, это тоже нормально. Обычно программистам с опытом работы на языке С больше нравится NULL, а программистам, привыкшим к C++,— значение 0.

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

i f

(р1 == р2) cout «

i f

(рЗ

== 0)

cout

«

i f

(р1

!= 0)

cout

«

"Одинаковый адрес, а не значения по адресу\п"; "Это нулевой указатель\п"; "Можно начинать работу\п";

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

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

| 263 |

Пусть р1 ссылается на целочисленную переменную v1, содержащую 123. Это целое. Та же звездочка использовалась при определении указателя: int *р1;, что не случайно. Здесь говорится не только, что р1 — указатель на целое, но и что *р1 — целое. Поэтому областью действия звездочки является только одно имя. При определении целочисленных переменных (или переменных любых других встроенных типов) имя типа охватывает любое число переменных. На­ пример, выше переменные v1 и v2 определяются с помощью одного ключевого слова int. С указателями это не работает. Следующая строка определяет один указатель и две целочисленные переменные:

i n t * pt1, pt2, pt3;

/ / pt1 - указатель, pt2 и pt3 - целые

Вернемся к разыменованию. Разыменованный указатель — это всего лишь переменная, на которую он ссылается.

*р1 = 42;

/ /

v1 теперь не 123, это 42

*р2

= 180;

/ /

v1 теперь не 42, это 180

*рЗ

= 42;

/ /

не разыменовывайте указателей NULL;

// это ведет к аварийному завершению программы.

Вданном примере р1 ссылается на v1, следовательно, *р1 и v1 — во всех отно­ шениях синонимы. До тех пор, пока указателю не будет присвоено что-то еще:

р1 = &v2;

/ /

р1 теперь ссылается на v2, а не на v1;

 

/ /

а *р1 означает 456

i f (*р1 == 456) *р1 = 42;

/ /

v2 больше не 456, это 42

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

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

void

add (int

х, int у, int

*z)

/ /

z - указатель на целое

{ *z

= X + у;

}

/ /

указатель

z теперь ссылается на другое место

Как такая функция add() должна вызываться в клиентской программе? Как получить присвоенное указателю значение? Это не может быть double, short или int. Оно должно быть равно NULL (что в данном случае бесполезно), быть равным другому указателю (возможно, в других примерах, но не здесь) или представлять адрес целого (т. е. то, что нужно). В качестве третьего аргумента передается адрес переменной, которая должна содержать сумму. Чтобы получить адрес, можно использовать операцию &. Вот как вызов с указателем должен выглядеть в кли­ ентском коде:

int а = 2,

b = 3,

с;

add(a, b,

&с)

/ / с не изменяется после вызова

Мы уже говорили, что вызов по Значению — естественный режим передачи параметров в С+4-. Это относится и к случаю передачи по указателю. По значе­ нию передается указатель, а не значение в клиентской области. Как и в случае передачи по значению, в области действия функции-сервера создается локальная копия указателя, которая затем инициализируется и в конечном счете уничтожается.

I 264 I

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

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

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

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

Тип указателя для параметра в заголовке функции

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

Следуйте данной логике, и все будет в порядке. Любое отклонение от этого перечня приведет к тому, что что-то пойдет не так. Не стоит тратить на это силы.

Рассмотрим еще один популярный пример: перестановку значений параметров. Если первый параметр больше второго, они меняются местами (переупорядочение в правильной последовательности — по возрастанию). Для перестановки значе­ ний параметров a1 и а2 значение первого сохраняется во временной переменной

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

temp. Затем можно скопировать значение а2 в a1, а значение

из временной переменной — в а2. То, что раньше было в а2,

После обмена значениями:

a1=42

а2=84

После вызова:

х=84

у=42

теперь окажется в a1. В листинге 7.1 показана реализация

 

 

 

функции swapO и ее KJшeнтcкoй функции main(). В целях

Рис. 7 . 1 . Вывод для программы

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

из листинга

7.1

 

на экран. Результаты выполнения представлены на рис,7.1.

Листинг 7.1. Передача параметров с побочными эффектами (плохая версия)

#inclucle

<iostream>

 

 

 

 

 

 

 

using

namespace std;

 

 

 

 

 

 

 

void

swap (int a1, int

a2)

'

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

{ int

temp;

 

 

 

 

 

 

 

 

i f

(a1

> a2)

 

 

 

 

 

 

 

{

cout

«

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

a1 «

" a2^" «

a2 «

endl;

 

temp = a1; a1 = a2;

a2 = temp;

 

 

 

 

 

 

cout «

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

a1 «

" a2=" «

a2 «

endl;}}

int

mainO

 

 

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

int

X = 84, у = 42;

 

/ /

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

 

swap(x,y);

 

/ /

неверный режим передачи параметров - не должно работать

cout «

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

у=" « у « endl;

 

 

return

0;

 

 

 

 

 

 

 

 

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

void

swap (int *a1, int

*a2)

/ / корректный

режим передачи

параметров

{ int temp;

 

 

 

 

 

i f

(a1

> a2)

 

 

 

 

{

cout

«

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

" a2=" « a2 «

endl;

 

temp = a1; a1 = a2;

a2 = temp;

 

 

 

 

cout

«

"После обмена значениями: a1=" « a1 « " a2=" « a2 « endl; }}

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

265

Хорошо смотрится, но тоже не летает. Оператор temp = a1; некорректен. Пере­ менная temp имеет тип int, а переменная a1 — нет. Это указатель на int. Одно другому присваивать нельзя. Но можно присвоить один указатель на целое друго­ му указателю на целое. Не позволяйте своему опыту работы с числовыми типами ввести вас в заблуждение. Разные числовые типы совместимы. Их можно при­ вести один к другому. Значения указателей — типы несовместимые, и такое пре­ образование не допускается. Их нельзя привести к типу, отличному от указателя.

Если вы получите такое сообш,ение, не отчаивайтесь, а просто подумайте, что должно быть в правой части присваивания. Переменная a1 — это не целое. Какая родственная переменная имеет целое значение? Посмотрите на список парамет­ ров. Где здесь целое? Да, int *a1, следовательно, *a1 и есть целое, а присваива­ ние должно выглядеть так: temp = *a1. Фактически, это не очень трудно, однако внесение изменений в тело функции — задача утомительная и способствуюш,ая ошибкам. Нужно убедиться, что выполняется разыменование a1 и а2, но не ис­ пользуется разыменование temp:

void

swap (int

*a1,

int ^а2)

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

{ int

temp;

 

 

 

 

 

 

 

 

 

i f

(a1

> a2)

{

 

 

 

 

 

 

 

cout

«

"Перед обменом: *a1=" «

*a1 «

"

*a2=" «

*a2 «

endl;

temp = *a1; *a1

= *a2;

*a2 = temp;

/ /

корректное разыменовывание

cout

«

"После обмена:

*a1=" «

*a1 «

"

*a2=" «

*a2 «

endl; } }

В версии функции swapO в листинге 7.1 компилятор сообш,ает о вызове swap(x,y). Действительно, перемен­ ная X имеет тип int, т. е. это адрес целочисленной пе­ ременной. Есть ли здесь смысл? Корректная версия программы показана в листинге 7.2, а соответствуюндий вывод — на рис. 7.2.

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

После

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

*a1=42 *а2=82

После

вызова:

х=42 у=82

Рис. 7 . 2 . Вывод для

программы

 

из листинга

7.2

Листинг 7.2. Передача параметров по указателю (режимы параметров корректны)

#include <iostream> using namespace std;

void

swap (int

*a1, int *a2)

/ / корректный

режим передачи параметров

{ int

temp;

 

 

 

i f

(a1 > a2)

"Перед обменом значениями: *a1=" << *a1 << " *a2=" « *a2 <<endl;

{ cout «

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

// корректное разыменовывание

cout «

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

*a1 « " *a2=" «

*a2 « endl; }}

int mainO

 

 

 

 

{

 

 

у = 42

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

int X = 82,

swap(&x,&y);

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

cout «

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

endl;

 

return 0;

 

 

 

}

 

 

 

 

 

Сделано ли все необходимое для тестирования данной программы? Она содер­ жит условный оператор, проверявшийся только один раз. Это простая маленькая программа, и стоит ли тратить время на проверку того, что и так ясно как божий день? Всего не проверишь. Тем не менее, когда дело касается передачи парамет­ ров, нельзя быть ни в чем уверенным (шаг влево, шаг вправо...). А потому изменим

266

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

 

функцию main() следующим образом, просто, чтобы убедиться, что когда аргумен­

 

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

 

i nt

mainO

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

 

/ / {

int

X = 82, у = 42

 

{ int X = 42, у = 84

 

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

 

swap(&x,&y);

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

 

 

 

 

// недолжно работать

 

cout «

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

"у=" у « endl;

 

return

0; }

 

 

Перед обменом значениями

*a1=42 *а2=84

Результат

выполнения показан на рис. 7.3. Теперь

придется задать два вопроса. Вопрос №1: видите ли вы

После обмена значениями:

*a1=84 *а2=42

После вызова:

х=84 у=42

проблему в результатах? Убедитесь, что это так. Слиш­

 

 

 

 

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

Рис. 7 . 3 . Вывод для

программы

так как заранее не записали ответ. Похоже, что програм­

 

из листинга

7.3

 

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

swap() имеется оператор if. Возможно, это означает, что этот оператор сравнивает не значения параметров, а что-то еш,е. Вопрос №2: ви­ дите ли вы проблему в исходном коде в листинге 7.2? Это уже труднее, поскольку нет методологии поиска данной ошибки.

Функция swap() содержит 10 операций *, но нужны еш.е две. При сравнении a1 и а2 компилятор не возражает, так как эти две переменные имеют один тип. Если требуется сравнить два адреса, на это у вас полное право, и если первый адрес больше второго, программа будет менять местами аргументы, независимо от их порядка. Компилятор не гадает за программиста, что именно нужно делать, и не выводит предупреждений. Корректная версия программы показана в листинге 7.3.

Листинг 7.3. Передача параметров по указателю (исправлено разыменование)

#inclucle <iostream> using namespace std;

void

swap (int *a1, int *a2)

 

//

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

{ int

temp;

 

 

// Ой-ой-ой

 

 

i f

(*a1

>

*a2){

 

*а2 «

endl;

cout

«

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

« *a1 «

" *а2=" «

temp = *a1; *al = *a2; *a2 = temp;

// корректное разыменовывание

cout «

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

81 «

" *а2=" «

*а2 «

endl;}}

int mainO

 

 

 

 

 

 

// { int X = 82. у = 42

 

/ /

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

значения

{ int X = 42, у = 84;

 

/ /

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

swap(&x,&y);

 

/ /

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

cout «

"После вызова: X=" « X « "y= « у «

endl;

 

 

return

0;

 

 

 

 

 

 

}

 

 

 

 

 

 

 

 

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

Мнемоническое правило при передаче параметров состоит в следуюш,ем: при выборе имени параметра нужно начать с этого имени со звездочкой и не забывать о звездочке при использовании имени во всех других случаях. Проблема в первой версии swap(), да и во второй тоже, состояла в том, что в качестве имен парамет­ ров использовались a1 и а2. Если бы с самого начала были выбраны и везде ис­ пользовались *a1 и *а2, то написание такой функции не представляло бы труда, особенно, если применить в операторе условия выражение *a1 > *а2.

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

[ 267

Но это сложнее, чем передача параметров по значению. Программисту нужно:

• Использовать обозначение указателя в заголовке функции (и прототипе)

Разыменовывать указатели в теле функции

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

Внаграду он получает "побочный эффект" — изменения в параметрах отражаются в фактических аргументах в клиентском коде

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

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

i nt mainO

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

{ int X = 82, у = 42;

int *р1

= &х, *р2 = &у;

// использование указателей для ссылки

swap(p1,p2);

// на значения

// нет операции получения адреса

cout «

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

х « " у=" « у « endl;

return 0; }

 

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

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

swap(&5, &(х+у));

/ /

не годится: для г-значений

 

/ /

нет операции получения адреса

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

int mainO

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

{ double X = 82, у = 42;

swap(&x,&y);

// нет преобразования из double* в int*

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

" у=" « у « endl;

return 0; }

 

Можно попытаться "уговорить" компилятор принять эти аргументы, применив в вызове функции явное приведение типа к int*:

swap((int*)&x, (int*)&y);

/ /

будет менять местами значения int,

 

/ /

а не double

268

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

Такой вызов будет компилироваться, но здесь сравниваются и меняются места­ ми (если это происходит) только части значений double, имеющие размер целого. Компилятор проглотил это, так как программист сказал ему: "Я знаю, что делаю". Однако то, что он делает, некорректно. Убедитесь, что при компиляции програм­ мы воспринимаемое компилятором действительно имеет смысл.

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

Передача параметров в C++ по ссылке

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

В отличие от указателя, переменная ссылочного типа может ссылаться только на один адрес памяти, причем того же типа, что и сама ссылка. Переменную-ссылку нельзя переназначить на другой адрес памяти, поэтому, в отличие от указателей, переменные-ссылки должны инициализироваться в определении. Если не сделать этого, то переменная ссылка вообще не будет никуда ссылаться и станет бесполеэ ной. Чтобы указать, что переменная является ссылкой, а не указателем, слева от ее имени помещается амперсанд (&), а не звездочка, как в указателе. После ини­ циализации ссылки к переменной, на которую эта ссылка указывает, не нужно применять никакие операции. Такое изменение в обозначении весьма разумно. Это одна из причин введения ссылок в C+ + .

i nt

v1=123,

v2=456;

/ /

целочисленные переменные, не обязательная

 

 

 

/ /

инициализация

int

*p1=&v1,

*p2=&v2;

/ /

указатели на int, не обязательная инициализация

int

&r1=v1,

&r2=v2;

/ /

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

Для указателей *р1 и v1 — синонимы. Нужна операция разыменования *. Для ссылок синонимами являются г1 и v1, и никаких операций не требуется. В C++ для ссылок нет операции разыменования, и это еще одна причина введе­ ния ссылок.

i f

(р1 !=

р2) cout

«

"Разные

адреса\п";

/ /

конечно,

&v1

!= &v2

i f

(*р1

!=

*р2) cout

« "Разные значения\п";

/ /

конечно,

123

!=

456

i f

(г1

!=

г2) cout

«

"Разные

значения\п";

/ /

конечно,

123

!=

456

При работе с указателями нет разницы, как получать значение — с помощью операции разыменования (например, *р1) или имени переменной (такого, как v1). При использовании ссылок все равно, как вы ссылаетесь на значение: по имени ссылки без какой-либо операции (например, г1) или по имени переменной (v1). Это синонимы:

*р1 = 42;

/ /

v1

г1)

содержат теперь

не 123,

а 42

г1 = 180;

/ /

v1

*р1)

содержат теперь не 42,

а 180

v1 = 42;

/ /

г1

*р1)

содержат снова

42

 

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

269

Возможно, это кажется несколько запутанным: указатели "разыменовывают­ ся", а не "разуказываются". С другой стороны, нельзя разыменовывать ссылки. Все придумано вовсе не для того, чтобы вас запутать. Просто так исторически сложилось. Еиде до появления ANSI С указатели часто называли ссылками, по­ скольку они ссылаются на переменные, а передачу по указателю называли переда­ чей по ссылке. Вместо термина "разуказание" (depointing) всегда использовался термин "разыменование" (dereferencing). Такие названия сохранились и доныне.

Данная терминология пережила стандартизацию ANSI. При разработке C+-f потребовались новые термины для таких понятий, как ссылки, доступ, указание, обозначение и пр. В процессе отбора был выбран термин "ссылка" (reference), так что теперь мы разыменовываем указатели, а не ссылки.

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

р1 = &v1;

/ /

р1 теперь ссылается не на v1, а на v2

р1 = р2;

/ /

еще один способ - тот же результат

г1 = v2;

/ /

г1 все равно указывает на v1, где теперь содержится 456

г1 = г2;

/ /

еще один способ переместить данные из v2 в v1

i f (r1==v2) r1 = 42;

 

/ / сравнение дает true, и v1 теперь содержит 42

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

void add(int

х, int у, int &z)

/ /

z - ссылка на целое

{ Z = X + у;

}

/ /

изменяются данные, на которые ссылается z

Здесь переменная z — ссылка на целое. Когда вызывается функция, для данно­ го параметра выделяется память, и он инициализируется адресом фактического аргумента. (Чуть позднее мы увидим, как это делается.) Присваивание z моди­ фицирует данные, на которые указывает ссылка, т. е. фактический аргумент. Видно, что тело функции выглядит в точности так же, как если бы параметр z передавался по значению. Применять разыменование нет необходимости. В за­ головке функции & указывает на передачу по ссылке. В вызове функции нужно инициализировать ссылку адресом, на который она ссылается. Как это сделать? Согласно синтаксису инициализации переменной, можно использовать имя переменной без операций. Следовательно, вызов функции в клиентском коде должен выглядеть так:

int а = 2,

b = 3,

с;

add(a, b,

с);

/ / с не изменяется после вызова

При передаче параметров по ссылке нужно задавать:

• Имена аргументов без операции получения адреса в вызове

• Тип ссылки для параметров в заголовке функции (и прототипе)

• Имена параметров без разыменования в теле функции

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

По существу эта конструкция напоминает язык Паскаль, где ключевое слово var играет роль операции & в С+Н-. Она указывает, что формальные параметры из­ меняются в теле функции, и эти изменения отражаются в значениях фактических

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