Жанр: Учеба
Правила программирования на Си и Си плюс плюс.
...ъекты по значению, исходя из логики.
Например, вы обычно в функцию передаете тип double, а не
указатель на double. Тем не менее, тип double представляет собой 8байтовую
упакованную структуру с тремя полями: знаковым битом,
мантиссой и порядком. Передавайте в этой ситуации ссылку на
константный объект.
+ Если объект какого-нибудь определенного пользователем класса
обычно передается по значению, то используйте вместо этого ссылку
на константный объект, чтобы избежать неявного вызова конструктора
копии.
Ссылки в языке не предназначены для имитации Паскаля и не должны
использоваться так, как используются в программе на Паскале.
Проблема ссылочных аргументов - сопровождение. В прошлом году
один из наших сотрудников написал следующую подпрограмму:
Правила программирования на Си++
178
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 );
Этот дополнительный знак & является решающим. Средний
сопровождающий программист полагает, что единственная причина
передачи адреса локальной переменной в другую функцию состоит в том,
чтобы разрешить функции модифицировать эту локальную переменную.
Другими словами, вариант с указателем является
самодокументирующимся; вы сообщаете своему читателю, что этот
объект изменяется функцией. Ссылочный аргумент не дает вам такой
информации.
Ссылки 179
Это не значит, что вы должны избегать ссылок. Четвертая причина в
начале этого раздела вполне законна: ссылки являются замечательным
способом избегать ненужных затрат на копирование, неявных при
передаче по значению. Тем не менее, для обеспечения безопасности
ссылочные аргументы должны всегда ссылаться на константные объекты.
Для данного прототипа:
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 ) )
p-"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*.
Вероятно, есть еще дюжина других возможностей. Все предыдущее
варианты обладают требуемым свойством - в них нет скрытых операций
Ссылки 181
и ясно, как происходит переход к "текущему положению".
122. Не возвращайте ссылки (или указатели) на
локальные переменные
Эта проблема проявляется и в Си, где вы не можете вернуть указатель на
локальную переменную. Не возвращайте ссылку на объект, который не
существует после этого возврата. Следующий код не работает:
some_class &f()
{
some_class x;
// ...
return x;
}
Действительной проблемой здесь является синтаксис Си++. Оператор
return
может располагаться на отдалении от определения возвращаемой
величины. Единственный способ узнать, что на самом деле делает
return
x
, - это взглянуть на заголовок функции и посмотреть,
возвращает она ссылку, или нет.
123. Не возвращайте ссылки на память, выделенную
оператором new
Каждый вызов new должен сопровождаться delete - подобно
malloc()
и free(). Я иногда видел людей, старающихся избежать
накладных расходов от конструкторов копии перегруженной бинарной
операции подобным образом:
const some_class &some_class::operator+( const some_class &r )
const
{
some_class *p = new some_class;
// ...
return *p;
}
Этот код не работает, потому что вы не можете вернуться к этой памяти,
чтобы освободить ее. Когда вы пишите:
some_class a, b, c;
c = a + b;
то a + b возвращает объект, а не указатель. Единственным способом
получить указатель, который вы можете передать в оператор delete,
является:
Правила программирования на Си++
some_class *p;
c = *(p = &(a + b));
Это даже страшно выговорить. Функция operator+() не может прямо
возвратить указатель. Если она выглядит подобным образом:
const some_class *some_class::operator+( const some_class &r )
const
{
some_class *p = new some_class;
// ...
return p;
}
то вы должны записать:
c = *(p = a + b);
что не так страшно, как в предыдущем примере, но все еще довольно
плохо. Единственное решение этой задачи состоит в том, чтобы стиснуть
зубы и вернуть объект:
const some_class some_class::operator+( const some_class &r )
const
{
some_class obj;
// ...
return obj;
}
Если вам удастся вызвать конструктор копии в операторе return, то
быть по сему.
Конструкторы, деструкторы и operator=( ) 183
и operator=( )
Функции конструкторов, деструкторов и операций operator=() имеют
ту особенность, что их создает компилятор в том случае, если не создаете
вы. Генерируемый компилятором конструктор по умолчанию (не
имеющий аргументов) и генерируемый компилятором деструктор нужны
для создания указателя на таблицу виртуальных функций (подробнее об
этом вскоре).
Генерируемый компилятором конструктор копии (чьим аргументом
является ссылка на текущий класс) нужен еще по двум причинам, кроме
таблицы виртуальных функций. Во-первых, код на Си++, который
выглядит как Си, должен и работать как Си. Так как правила
копирования, которые относятся к классу, относятся также и к структуре,
поэтому компилятор будет вынужден обычно генерировать конструктор
копии в структуре, чтобы обрабатывать копирование структур в стиле Си.
Этот конструктор копии используется явно подобным образом:
some_class x; // конструктор по умолчанию
some_class y = x; // конструктор копии
но кроме этого он используется и неявно в двух ситуациях. Первой
является вызов по значению:
some_class x;
f( some_class x ) // передается по значению, а не по ссылке.
f( x ); // вызывается конструктор копии для передачи x
// по значению. Оно должно скопироваться в стек.
Второй является возврат по значению:
some_class g() // Помните, что x - локальная, автоматическая
// переменная.Она исчезает после возвращения
// функцией значения.
{
some_class x; // Оператор return после этого должен
return x; // скопировать x куда-нибудь в надежное место
} // (
обычно в стек после аргументов).Он
// использует для этой цели конструктор копии.
Генерируемая компилятором функция-операция operator=() нужна
лишь для поддержки копирования структур в стиле Си там, где не
определена операция присваивания.
Правила программирования на Си++
124. Операция operator=( ) должна возвращать
ссылку на константу
125. Присваивание самому себе должно работать
Определение operator=( ) должно всегда иметь следующую форму:
class class_name
{
const class_name &operator=( const class_name &r );
};
const class_name &class_name::operator=( const class_name &r )
{
if
( this != &r )
{
// здесь скопировать
}
return *this;
}
Аргумент, представляющий операнд источника данных, является
ссылкой, чтобы избежать накладных расходов вызова по значению; это
ссылка на константу, потому что аргумент не предназначен для
модификации.
Эта функция возвращает ссылку, потому что она может это сделать. То
есть вы могли бы удалить & из объявления возвращаемой величины, и все
бы работало прекрасно, но вы бы получили ненужный вызов
конструктора копии, вынужденный возвратом по значению. Так как у нас
уже есть объект, инициализированный по типу правой части (*this), то
мы просто можем его вернуть. Даже если возврат объекта вместо ссылки
в действительности является ошибкой для функции operator=(),
компилятор просто выполнит то, что вы ему приказали. Здесь не будет
сообщения об ошибке; и на самом деле все будет работать. Код просто
будет выполняться более медленно, чем нужно.
Наконец, operator=() должен возвращать ссылку на константу
просто потому, что не хотите, чтобы кто-нибудь имел возможность
модифицировать возвращенный объект после того, как произошло
присваивание. Следующее будет недопустимым в случае возврата ссылки
на константу:
(x =y) = z;
Причина состоит в том, что (x=y) расценивается как возвращаемое
значение функции operator=(), т.е. константная ссылка. Получателем
сообщения =z является объект, только что возвращенный от x=y. Тем не
Конструкторы, деструкторы и operator=( ) 185
менее, вы не можете послать сообщение operator=() константному
объекту, потому что его объявление не имеет в конце const:
// НЕ ДЕЛАЙТЕ ЭТОГО В ФУНКЦИИ
// С ИСПОЛЬЗОВАНИЕМ operator=().
// |
// V
const class_name &operator=( const class_name &r ) const;
Компилятор должен выдать вам ошибку типа "не могу преобразовать
ссылку на переменную в ссылку на константу", если вы попробуете
(x=y)=z
Другим спорным моментом в предыдущем коде является сравнение:
if
( this != &r )
в функции operator=(). Выражение:
class_name x;
// ...
x = x;
должно всегда срабатывать, и сравнение this с адресом входного правого
аргумента является простым способом в этом убедиться. Имейте в виду,
что многие алгоритмы полагают самоприсваивание безвредным, поэтому
не делайте его особым случаем. Также имейте в виду, что
самоприсваивание могло бы быть затушевано при помощи указателя, как
в:
class_name array[10];
class_name *p = array;
// ...
*p = array[0];
126. Классы, имеющие члены-указатели, должны
всегда определять конструктор копии и функцию
operator=()
Если класс не определяет методы копирования - конструктор копии и
функцию operator=(), то это делает компилятор. Созданный
компилятором конструктор должен выполнять "почленное" копирование,
которое осуществляется таким образом, как будто вы написали
this
-"field = src.field
для каждого члена. Это означает, что
теоретически должны вызываться конструкторы копий и функции
operator
=()
вложенных объектов и базовых классов. Даже если все
работает правильно, все же указатели копируются как указатели. То есть,
строка string, представленная как char*, - не строка, а указатель, и
Правила программирования на Си++
будет скопирован лишь указатель. Представьте, что определение string
на листинге 7 со страницы 155 не имеет конструктора копии или функции
operator
=()
. Если вы запишите
string s1 = "
фу", s2;
// ...
s2 = s1;
то это присваивание вместо поля указателя s2 запишет указатель от s1.
Та память, которая была адресована посредством s1-"buf, теперь
потеряна, то есть у вас утечка памяти. Хуже того, если вы меняете s1, то
s2
меняется также, потому что они указывают на один и тот же буфер.
Наконец, когда строки выходят из области действия, они обе передают
buf
для освобождения, по сути, очищая его область памяти дважды, и,
вероятно, разрушают структуру динамической памяти. Решайте эту
проблему путем добавления конструктора копии и функции
operator
=()
, как было сделано на листинге 7 со страницы 155. Теперь
копия будет иметь свой собственный буфер с тем же содержанием, что и у
буфера строки-источника.
Последнее замечание: я выше написал "должен выполнять" и
"теоретически" в первом абзаце, потому что встречал компиляторы,
которые фактически выполняли функцию memcpy() в качестве операции
копирования по умолчанию, просто как это бы сделал компилятор Си. В
этом случае конструктор копии и функция operator=() вложенных
объектов не будут вызваны, и вы всегда будете должны обеспечивать
конструктор копии и функцию operator=() для копирования
вложенных объектов. Если вы желаете достигнуть при этом абсолютной
надежности, вы будете должны проделать это для всех классов, чьи члены
не являются основными числовыми типами Си++.
127. Если у вас есть доступ к объекту, то он должен
быть инициализирован
128. Используйте списки инициализации членов
129. Исходите из того, что члены и базовые классы
инициализируются в случайном порядке
Многие неопытные программисты на Си++ избегают списков
инициализации членов, как я полагаю, потому, что они выглядят так
причудливо. Фактом является то, что большинство программ, которые их
не используют, попросту некорректны. Возьмите, например, следующий
код (определение строкового класса из листинга 7 со страницы 155):
Конструкторы, деструкторы и operator=( ) 187
class base
{
string s;
public
:
base( const char *init_value );
}
//------------------------------
base::base( const char *init_value )
{
s = init_value;
}
Основной принцип такой: если у вас есть доступ к объекту, то он должен
быть инициализирован. Так как поле s видимо для конструктора base, то
Си++ гарантирует, что оно инициализировано до окончания выполнения
тела конструктора. Список инициализации членов является механизмом
выбора выполняемого конструктора. Если вы его опускаете, то получите
конструктор по умолчанию, у которого нет аргументов, или, как в случае
рассматриваемого нами класса string, такой, аргументы которого
получают значения по умолчанию. Следовательно, компилятор вначале
проинициализирует s пустой строкой, разместив односимвольную строку
при помощи new и поместив в нее \0. Затем выполняется тело
конструктора и вызывается функция string::operator=(). Эта
функция освобождает только что размещенный буфер, размещает буфер
большей длины и инициализирует его значением init_value. Ужасно
много работы. Лучше сразу проинициализировать объект корректным
начальным значением. Используйте:
base( const char *init_value ) : s(init_value)
{}
Теперь строка s будет инициализирована правильно, и не нужен вызов
operator
=()
для ее повторной инициализации.
Настоящее правило также применимо к базовым классам, доступным
из конструктора производного класса, поэтому они должны
инициализироваться до выполнения конструктора производного класса.
Базовые классы инициализируются перед членами производного класса,
потому что члены производного класса невидимы в базовом классе.
Подведем итог - объекты инициализируются в следующем порядке:
+ Базовые классы в порядке объявления.
+ Поля данных в порядке объявления.
Лишь затем выполняется конструктор производного класса. Одно
последнее предостережение. Заметьте, что порядок объявления управляет
порядком инициализации. Порядок, в котором элементы появляются в
Правила программирования на Си++
списке инициализации членов, является несущественным. Более того,
порядок объявления не должен рассматриваться как неизменный.
Например, вы можете изменить порядок, в котором объявлены поля
данных. Рассмотрим следующее определение класса где-нибудь в
заголовочном файле:
class wilma
{
int y;
int x;
public
:
wilma( int ix );
};
Вот определение конструктора в файле .c:
wilma::wilma( int ix ) : y(ix * 10), x(y + 1)
{}
Теперь допустим, что какой-то сопровождающий программист переставит
поля данных в алфавитном порядке, поменяв местами x и y. Этот
конструктор больше не работает: поле x инициализируется первым,
потому что оно первое в определении класса, и инициализируется
значением y+1, но поле y еще не инициализировалось.
Исправьте код, исключив расчет на определенный порядок
инициализации:
wilma::wilma( int ix ) : y(ix * 10), x((ix *10) + 1)
{}
130. Конструкторы копий должны использовать
списки инициализации членов
У наследования тоже есть свои проблемы с копированием. Конструктор
копии все же остается конструктором, поэтому здесь также применимы
результаты обсуждения предыдущего правила. Если у конструктора
копии нет списка инициализации членов, то для базовых классов и
вложенных объектов используется конструктор по умолчанию. Так как
список инициализации членов отсутствует в следующем определении
конструктора копии, то компонент базового класса в объекте
производного класса инициализируется с использованием base(void), а
поле s инициализируется с использованием string::string(void):
class base
{
public
:
base( void ); // конструктор по умолчанию
base( const base &r ); // конструктор копии
Конструкторы, деструкторы и operator=( ) 189
const base &operator=( const base &r );
};
class derived
{
string s; // класс имеет конструктор копии
public
:
derived( const derived &r )
};
derived::derived( const derived &r )
{}
Чтобы гарантировать копирование также поля string и компонента
базового класса в объекте производного класса, используйте следующее:
derived::derived( const derived &r ) : base(r), s(r.s) {}
131. Производные классы должны обычно определять
конструктор копии и функцию operator=( )
При наследовании есть и другая связанная с копированием проблема. В
одном месте руководства10 по языку Си++ недвусмысленно заявлено:
"конструкторы и функция operator=() не наследуются". Однако далее
в этом же документе говорится, что существуют ситуации, в которых
компилятор не может создать конструктор копии или функцию
operator
=()
, которые бы корректно вызывались вслед за функциями
базового класса. Так как нет практической разницы между
унаследованной и сгенерированной функциями operator=(), которые
ничего не делают, кроме вызова функции базового класса, то эта
неопределенность вызвала много бед.
Я наблюдал два полностью несовместимых поведения компиляторов,
столкнувшихся с этой дилеммой. Некоторые компиляторы считали
правильным, чтобы сгенерированные компилятором конструкторы копий
и функции operator
=()
вызывались автоматически после
конструкторов и функций operator=() базового класса (и вложенного
10 Книга Эллис и Страуструпа "The Annotated C++ Reference Manual" (Reading:
Addison Wesley, 1990), использованная в качестве базового документа комитетом
ISO/ANSI по Си++.
Имеется перевод на русский язык под редакцией А.Гутмана "Справочное
руководство по языку программирования Си++ с комментариями" (М.: Мир, 1992).
-Прим.перев.
Правила программирования на Си++
объекта).11 Это как раз тот способ, который, по мнению большинства,
реализуется языком программирования. Другими словами, со следующим
кодом проблем не будет:
class base
{
public
:
base( const base &r );
const base &operator=( const base &r );
};
class derived : public base
{
string s;
// нет операции operator=() или конструктора копии
};
derived x;
derived y = x; // вызывает конструктор копии базового класса
// для копирования базового класса. Также
// вызывает конструктор копии строки для
// копирования поля s.
x = y; // вызывает функцию базового класса operator=() для
// копирования базового класса. Также вызывает
// строковую функцию operator=() для копирования поля s.
Если бы все компиляторы работали таким образом, то проблемы бы не
было. К несчастью, некоторые компиляторы принимают ту самую
директиву "не наследуются" за чистую монету. Только что
представленный код не будет работать с этими компиляторами. В них
сгенерированные компилятором конструктор копии и функция
operator
=()
производного класса действуют так, как будто бы их
эквиваленты в базовом классе (и вложенном объекте) просто не
существуют. Другими словами, конструктор по умолчанию - без
аргументов - вызывается для копирования компонента базового класса,
а почленное копирование - которое может выполняться просто
функцией memcpy() - используется для поля. Мое понимание
пересмотренного проекта стандарта Си++ ISO/ANSI позволяет сделать
вывод, что такое поведение некорректно, но в течение некоторого
времени вам придется рассчитывать на худшее, чтобы обеспечивать
переносимость. Следовательно, это, вероятно, хорошая мысль - всегда
помещать в производный класс конструктор копии и функцию
operator
=()
, которые явно вызывают своих двойников из базового
11 Конечно, конструкторы копий и функции operator=(), создаваемые вами (в
отличие от компилятора), никогда не вызывают своих двойников из базового класса
автоматически.
Конструкторы, деструкторы и operator=( ) 191
класса. Вот реализация предыдущего производного класса для самого
худшего случая:
class derived : public base
{
string s;
public
:
der
...Поділитися у соцмережах