Правила программирования на С и С++. Главы 7-8
Страница 36. Глава 8.В. Ссылки


Глава 8.В. Ссылки

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

 

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

 

Использование ссылочных аргументов в языке программирования вызвано четырьмя причинами:

  • Они нужны вам для определения конструктора копии.
  • Они нужны вам для определения перегруженных операций. Если вы определили:
some_class *operator+( some_class *left, some_class *right );то вы должны сделать такое дополнение: some_class x, y;

x = *(?x + ?y)

Использование ссылок для аргумента и возвращаемого значения позволяет вам написать: x = x + 1;
  • Вы часто хотите передать объекты по значению, исходя из логики. Например, вы обычно в функцию передаете тип double, а не указатель на double. Тем не менее, тип double представляет собой 8-байтовую упакованную структуру с тремя полями: знаковым битом, мантиссой и порядком. Передавайте в этой ситуации ссылку на константный объект.
  • Если объект какого-нибудь определенного пользователем класса обычно передается по значению, то используйте вместо этого ссылку на константный объект, чтобы избежать неявного вызова конструктора копии.
Ссылки в языке не предназначены для имитации Паскаля и не должны использоваться так, как используются в программе на Паскале.

Проблема ссылочных аргументов - сопровождение. В прошлом году один из наших сотрудников написал следующую подпрограмму:

void copy_word( char *target, char *?src ) // src является ссылкой на char*

{

while( isspace(*src) ) ++src; // Инкрементировать указатель, // на который ссылается src.while( *src ?? !isspace(*src) ) *target++ = *src++; // Передвинуть указатель, // на который ссылается src,

// за текущее слово.

}Автор полагал, что вы будете вызывать copy_word() многократно. Каждый раз подпрограмма копировала бы следующее слово в буфер target и продвигала бы указатель в источнике.

Вчера вы написали следующий код:

f( const char *p )

{

char *p = new char[1024];

load( p );

char word[64];

copy_word( word, p );

delete( p ); // Сюрприз! p был модифицирован, поэтому

} // весь этот участок памяти обращается в кучу мусора!Главная проблема состоит в том, что, глядя на вызов copy_word( word, p ), вы не получаете подсказки о возможном изменении p в подпрограмме. Чтобы добраться до этой информации, вы должны взглянуть на прототип этой функции (который, вероятно, скрыт на 6-ом уровне вложенности в заголовочном файле). Огромные проблемы при сопровождении.

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

copy_word( word, ?p );Этот дополнительный знак ? является решающим. Средний сопровождающий программист полагает, что единственная причина передачи адреса локальной переменной в другую функцию состоит в том, чтобы разрешить функции модифицировать эту локальную переменную. Другими словами, вариант с указателем является самодокументирующимся; вы сообщаете своему читателю, что этот объект изменяется функцией. Ссылочный аргумент не дает вам такой информации.

Это не значит, что вы должны избегать ссылок. Четвертая причина в начале этого раздела вполне законна: ссылки являются замечательным способом избегать ненужных затрат на копирование, неявных при передаче по значению. Тем не менее, для обеспечения безопасности ссылочные аргументы должны всегда ссылаться на константные объекты. Для данного прототипа:

f( const some_class ?obj );этот код вполне законен: some_class an_object;

f( an_object );

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

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

  • являются объектами какого-то класса (в отличие от основных типов, подобных int);
  • не модифицируются где-то внутри функции.
Объекты, которые передаются по значению и затем модифицируются внутри функции, конечно должны по-прежнему передаваться по значению.

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

CDocument *doc;

CView *view;

POSITION pos = doc->GetFirstViewPosition();

while( view = GetNextView(pos) )

view->Invalidate();Здесь есть две проблемы. Во-первых, у функции GetNextView() неудачное имя. Она должна быть названа GetCurrentViewAndAdvancePosition(), потому что она на самом деле возвращает текущий элемент и затем продвигает указатель положения (который является ссылочным аргументом результата) на следующий элемент. Что приводит нас к второй проблеме: средний читатель смотрит на предыдущий код и задумывается над тем, как завершается этот цикл. Другими словами, здесь скрывается сюрприз. Операция итерации цикла скрыта в GetNextView(pos), поэтому неясно, где она происходит. Ситуация могла быть хуже, если бы цикл был больше и содержал бы несколько функций, использующих pos в качестве аргумента - вы бы не имели никакого представления о том, какая из них вызывает перемещение.

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

POSITION pos = doc->GetFirstViewPosition();

while( p = GetNextView( ?pos ) )

view->Invalidate();Таким способом ?pos сообщает вам, что pos будет модифицироваться; иначе зачем передавать указатель? Тем не менее, существуют и лучшие решения. Вот первое: for( CView *p = doc->GetFirstView(); p ; p = p->NextView() ) p->Invalidate();Вот второе: POSITION pos = doc->GetFirstViewPosition();

for( ; pos ; pos = doc->GetNextView(pos) )

(pos->current())->Invalidate();Вот третье: CPosition pos = doc->GetFirstViewPosition();

for( ; pos; pos.Advance() )

( pos->CurrentView() )->Invalidate();Вот четвертый: ViewListIterator cur_view = doc->View_list(); // Просмотреть весь
список отображений этого документа
for( ; cur_view ; ++cur_view ) // ++ переходит к следующему отображению. cur_view->Invalidate(); // -> возвращает указатель View*.

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

 
« Предыдущая статья   Следующая статья »