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

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

692

extern Account

consolidate_accts( const vector< Account >& )

{

Account final_acct;

// выполнить финансовую операцию return final_acct;

}

// вызывается пять копирующих конструкторов класса string

∙ определение непустого последовательного контейнера: vector< string > svec( 5 );

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

вставка объекта класса в контейнер:

svec.push_back( string( "pooh" ));

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

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

Одно из решений псевдонимов указателей заключается в том, чтобы выделить область памяти для копии строки и инициализировать newAcct._name адресом этой области. Следовательно, почленную инициализацию по умолчанию для класса Account нужно подавить за счет предоставления явного копирующего конструктора, который реализует правильную семантику инициализации.

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

С++ для начинающих

693

inline Account::

Account( const Account &rhs )

{

//решить проблему псевдонима указателя

_name = new char[ strlen(rhs._name)+1 ]; strcpy( _name, rhs._name );

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

_acct_nmbr = get_unique_acct_nmbr();

//копирование этого члена и так работает

_balance = rhs._balance;

}

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

1.Объявить копирующий конструктор закрытым членом. Это предотвратит почленную инициализацию всюду, кроме функций-членов и друзей класса.

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

class Account { public:

Account();

Account( const char*, double=0.0 );

// ...

private:

Account( const Account& );

// ...

Чтобы запретить почленную инициализацию, класс Account можно объявить так:

};

14.6.1. Инициализация члена, являющегося объектом класса

Что произойдет, если в объявлении _name заменить C-строку на тип класса string? Как это повлияет на почленную инициализацию по умолчанию? Как надо будет изменить явный копирующий конструктор? Мы ответим на эти вопросы в данном подразделе.

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

newAcct._name = oldAcct._name;

С++ для начинающих

694

Члены, являющиеся объектами классов, обрабатываются по-другому. В инструкции

Account newAcct( oldAcct );

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

Таким образом, если обнаруживается член-объект класса, то описанный выше процесс применяется рекурсивно. У класса есть явный копирующий конструктор? Если да, вызвать его для задания начального значения члена-объекта класса. Иначе применить к этому члену почленную инициализацию по умолчанию. Если все члены этого класса принадлежат к встроенным или составным типам, то каждый инициализируется непосредственно и процесс на этом завершается. Если же некоторые члены сами являются объектами классов, то алгоритм применяется к ним рекурсивно, пока не останется ничего, кроме встроенных и составных типов.

В нашем примере у класса string есть явный копирующий конструктор, поэтому _name инициализируется с помощью его вызова. Копирующий конструктор по умолчанию для

inline Account::

Account( const Account &rhs )

{

_acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance;

//Псевдокод на C++

//иллюстрирует вызов копирующего конструктора

//для члена, являющегося объектом класса

_name.string::string( rhs._name );

класса Account выглядит следующим образом (хотя явно он не определен):

}

Теперь почленная инициализация по умолчанию для класса Account корректно обрабатывает выделение и освобождение памяти для _name, но все еще неверно копирует номер счета, поэтому приходится кодировать явный копирующий конструктор. Однако

// не совсем правильно...

inline Account::

Account( const Account &rhs )

{

_name = rhs._name; _balance = rhs._balance;

_acct_nmbr = get_unique_acct_nmbr();

приведенный ниже фрагмент не совсем правилен. Можете ли вы сказать, почему?

}

Эта реализация ошибочна, поскольку в ней не различаются инициализация и присваивание. В результате вместо вызова копирующего конструктора string мы

вызываем конструктор string по умолчанию на фазе неявной инициализации и копирующий оператор присваивания string в теле конструктора. Исправить это несложно:

С++ для начинающих

695

inline Account::

Account( const Account &rhs ) : _name( rhs._name )

{

_balance = rhs._balance;

_acct_nmbr = get_unique_acct_nmbr();

}

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

Упражнение 14.13

Для какого определения класса скорее всего понадобится копирующий конструктор?

1.Представление Point3w, содержащее четыре числа с плавающей точкой.

2.Класс matrix, в котором память для хранения матрицы выделяется динамически в конструкторе и освобождается в деструкторе.

3.Класс payroll (платежная ведомость), где каждому объекту приписывается уникальный идентификатор.

4.Класс word (слово), содержащий объект класса string и вектор, в котором хранятся пары (номер строки, смещение в строке).

Упражнение 14.14 Реализуйте для каждого из данных классов копирующий конструктор, конструктор по

(a) class BinStrTreeNode { public:

// ...

private:

string _value; int _count;

BinStrTreeNode *_leftchild; BinStrTreeNode *_rightchild;

умолчанию и деструктор.

(b) class BinStrTree { public:

// ...

private:

BinStrTreeNode *_root;

};

};

С++ для начинающих

696

(c)class iMatrix { public:

// ...

private:

int _rows; int _cols; int *_matrix;

(d)class theBigMix { public:

// ...

private:

BinStrTree _bst; iMatrix _im; string _name; vectorMfloat> *_pvec;

};

};

Упражнение 14.15

Нужен ли копирующий конструктор для того класса, который вы выбрали в упражнении 14.3 из раздела 14.2? Если нет, объясните почему. Если да, реализуйте его.

Упражнение 14.16

Идентифицируйте в следующем фрагменте программы все места, где происходит

Point global;

Point foo_bar( Point arg )

{

Point local = arg;

Point *heap = new Point( global ); *heap = local;

Point pa[ 4 ] = { local, *heap }; return *heap;

почленная инициализация:

}

14.7. Почленное присваивание A

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

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

newAcct = oldAcct;

С++ для начинающих

697

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

соответственного члена oldAcct. Компилятор генерирует

следующий копирующий

inline Account& Account::

operator=( const Account &rhs )

{

_name = rhs._name; _balance = rhs._balance;

_acct_nmbr = rhs._acct_nmbr;

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

}

Как правило, если для класса не подходит почленная инициализация по умолчанию, то не подходит и почленное присваивание по умолчанию. Например, для первоначального определения класса Account, где член _name был объявлен как char*, такое присваивание не годится ни для _name, ни для _acct_nmbr.

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

// общий вид копирующего оператора присваивания className&

className::

operator=( const className &rhs )

{

// не надо присваивать самому себе if ( this != &rhs )

{

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

}

// вернуть объект, которому присвоено значение return *this;

где будет реализована подходящая для класса семантика:

}

Здесь условная инструкция

if ( this != &rhs )

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

С++ для начинающих

698

Account&

Account::

operator=( const Account &rhs )

{

// не надо присваивать самому себе if ( this != &rhs )

{

delete [] _name;

_name = new char[strlen(rhs._name)+1]; strcpy( _name,rhs._name );

_balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr;

}

return *this;

}

Когда один объект класса присваивается другому, как, например, в инструкции:

newAcct = oldAcct;

выполняются следующие шаги:

1.Выясняется, есть ли в классе явный копирующий оператор присваивания.

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

3.Оператор вызывается для выполнения присваивания; если же он недоступен, компилятор выдает сообщение об ошибке.

4.Если явного оператора нет, выполняется почленное присваивание по умолчанию.

5.При почленном присваивании каждому члену встроенного или составного члена объекта в левой части присваивается значение соответственного члена объекта в правой части.

6.Для каждого члена, являющегося объектом класса, рекурсивно применяются шаги 1- 6, пока не останутся только члены встроенных и составных типов.

Если мы снова модифицируем определение класса Account так, что _name будет иметь тип string, то почленное присваивание по умолчанию

newAcct = oldAcct;

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

inline Account& Account::

operator=( const Account &rhs )

{

_balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr;

// этот вызов правилен и с точки зрения программиста name.string::operator=( rhs._name );

присваивания:

}

С++ для начинающих

699

Однако почленное присваивание по умолчанию для объектов класса Account не подходит из-за _acct_nmbr. Нужно реализовать явный копирующий оператор

Account&

Account::

operator=( const Account &rhs )

{

// не надо присваивать самому себе if ( this != &rhs )

{

// вызывается string::operator=( const string& ) _name = rhs._name;

_balance = rhs._balance;

}

return *this;

присваивания с учетом того, что _name это объект класса string:

}

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

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

Упражнение 14.17

Реализуйте копирующий оператор присваивания для каждого из классов, определенных в упражнении 14.14 из раздела 14.6.

Упражнение 14.18

Нужен ли копирующий оператор присваивания для того класса, который вы выбрали в упражнении 14.3 из раздела 14.2? Если да, реализуйте его. В противном случае объясните, почему он не нужен.

14.8. Соображения эффективности A

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

bool sufficient_funds( Account acct, double );

то при каждом ее вызове требуется выполнить почленную инициализацию формального параметра acct значением фактического аргумента-объекта класса Account. Если же

bool sufficient_funds( Account *pacct, double );

функция имеет любую из таких сигнатур:

bool sufficient_funds( Account &acct, double );

С++ для начинающих

700

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

Хотя возвращать указатель или ссылку на объект класса также более эффективно, чем сам объект, но корректно запрограммировать это достаточно сложно. Рассмотрим такой

//задача решается, но для больших матриц эффективность может

//оказаться неприемлемо низкой

Matrix

operator+( const Matrix& m1, const Matrix& m2 )

{

Matrix result;

// выполнить арифметические операции ...

return result;

оператор сложения:

}

Matrix a, b;

//...

//в обоих случаях вызывается operator+() Matrix c = a + b;

Этот перегруженный оператор позволяет пользователю писать a = b + c;

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

//более эффективно, но после возврата адрес оказывается недействительным

//это может привести к краху программы

Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

{

Matrix result;

// выполнить сложение ...

return result;

Следующая пересмотренная реализация намного увеличивает скорость:

}

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

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

С++ для начинающих

701

//нет возможности гарантировать отсутствие утечки памяти

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

Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

{

Matrix *result = new Matrix; // выполнить сложение ...

return *result;

}

Однако это неприемлемо: происходит большая утечка памяти, так как ни одна из частей

программы не отвечает за применение оператора delete к объекту по окончании его использования.

Вместо оператора сложения лучше применять именованную функцию, которой в качестве

//это обеспечивает нужную эффективность,

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

mat_add( Matrix &result,

const Matrix& m1, const Matrix& m3 )

{

// вычислить результат

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

}

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

// более не поддерживается

объекты

Matrix c = a + b;

// тоже не поддерживается

ииспользовать их в выражениях: if ( a + b > c ) ...

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