Инициализация - это что такое


Список параметров в определении и прототипе функции, кроме согласования типов параметров, имеет ещё одно назначение.

Объявление параметра может содержать инициализатор, то есть выражение, которое должно обеспечить параметру присвоение начального значения. Инициализатор параметра не является константным выражением. Начальная инициализация параметров происходит не на стадии компиляции (как, например, выделение памяти под массивы), а непосредственно в ходе выполнения программы.

Следующие строки демонстрируют пример объявления функции с инициализацией параметров. Для инициализации параметра ww используется функция XX.

Int BigVal;
int XX(int);
int ZZ(int tt, int ww = XX(BigVal));

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

Int ZZ(int tt, int = XX(BigVal));

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

Прототипы функции могут располагаться в различных областях видимости. Его можно даже разместить в теле определяемой функции. Каждое объявление функции может содержать собственные варианты объявления и инициализации параметров. Но во множестве объявлений одной и той же функции в пределах одной области видимости не допускается повторная инициализация параметров. Всему должен быть положен разумный предел.

Кроме того, в C++ действует ещё одно ограничение, связанное с порядком инициализации параметров в пределах области видимости. Инициализация проводится непременно с самого последнего (самого правого) параметра в списке объявлений параметров. Инициализация параметров не допускает пропусков: инициализированные параметры не могут чередоваться с параметрами неинициализированными.

Int MyF1 (int par1, int par2, int par3, int par4 = 10);
int MyF1 (int par1, int par2 = 20, int par3 = 20, int par4);
int MyF1 (int par1 = 100, int, int, int);

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

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

33передача параметров функции main c++

При создании консольного приложения в языке программирования С++, автоматически создается строка очень похожая на эту:

int main(int argc, char* argv) // параметры функции main()

Эта строка - заголовок главной функции main(), в скобочках объявлены параметры argс и argv. Так вот, если программу запускать через командную строку, то существует возможность передать какую-либо информацию этой программе, для этого и существуют параметры argcи argv. Параметр argc имеет тип данных int, и содержит количество параметров, передаваемых в функцию main. Причем argc всегда не меньше 1, даже когда мы не передаем никакой информации, так как первым параметром считается имя функции. Параметр argv это массив указателей на строки. Через командную строку можно передать только данные строкового типа. Указатели и строки - это две большие темы, под которые созданы отдельные разделы. Так вот именно через параметр argv и передается какая-либо информация. Разработаем программу, которую будем запускать через командную строку Windows, и передавать ей некоторую информацию.

Время жизни и область видимости программных объектов Классы памяти.

Время жизни переменной (глобальной или локальной) определяется по следующим правилам.

1. Переменная, объявленная глобально (т.е. вне всех блоков), существует на протяжении всего времени выполнения программы.

2. Локальные переменные (т.е. объявленные внутри блока) с классом памяти register или auto, имеют время жизни только на период выполнения того блока, в котором они объявлены. Если локальная переменная объявлена с классом памяти static или extern, то она имеет время жизни на период выполнения всей программы.

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

1. Переменная, объявленная или определенная глобально, видима от точки объявления или определения до конца исходного файла. Можно сделать переменную видимой и в других исходных файлах, для чего в этих файлах следует ее объявить с классом памяти extern.

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

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

4. Функции с классом памяти static видимы только в исходном файле, в котором они определены. Всякие другие функции видимы во всей программе.

Метки в функциях видимы на протяжении всей функции.

Имена формальных параметров, объявленные в списке параметров прототипа функции, видимы только от точки объявления параметра до конца объявления функции.

Классы памяти в с++.

2) register

3) static

4) extern

auto

int i; -> auto int i;

Классы памяти в с++.

Существует 4 спецификатора класса памяти:

2) register

3) static

4) extern

Если класс памяти не указан, он определяется по умолчанию из контекста объявления.

Объекты классов auto и register имеют локальное время жизни.

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

На внутреннем уровне при объявлении переменной может быть использован любой из 4 спецификаторов класса памяти. Если спецификатор опущен, то подразумевается auto

int i; -> auto int i;

Переменная с классом памяти auto имеет локальное время жизни. Она видима только в том блоке, в котором объявлена. Память для этой переменной выделяется при входе в блок и освобождается при выходе. При повторном входе в блок память для этой переменной может быть распределена в другом месте.

register

Предписывает компилятор распределить память под переменную в регистре процессора, если это возможно. Переменная с классом памяти register имеет ту же область видимости, что и auto.

Когда компилятор встречает спецификатор register, а свободного регистра не имеется, то для переменной распределяется память auto.

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

static

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

extern

Переменная, объявленная со спецификатором extern, является ссылкой на переменную с тем же именем, определенную на внешнем уровне в любом исходном файле программы.

Цель объявления extern состоит в том, чтобы сделать объявление переменной внешнего уровня видимой внутри блока.

35Инициализация глобальных и локальных переменных

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

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

37Динамические массивы. Особенности выделения и освобождения памяти для многомерных массивов.

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

Динамические массивы ограниченного размера и их объём

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

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

38Директивы препроцессора. Макроопределения.

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

#define, #elif, #else, #endif, #if, #ifdef, #ifndef, #include, #undef.

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

#define <идентификатор> <текст>

#define <идентификатор> (<список параметров>) <текст>

Директива #undef отменяет определение, введенное ранее директивой #define. Предположим, что на каком-либо участке программы нужно отменить определение константы FOUR. Это достигается следующей командой:

Отличие директивы #if от директив #ifdef и #ifndef заключается в возможности проверки более разнообразных условий, а не только существует или нет какие-либо константы. Например, с помощью директивы #if можно проводить такую проверку:

Используемая в приведенных примерах директива #include позволяет добавлять в программу ранее написанные программы и сохраненные в виде файлов. Например, строка

#include < stdio.h >

Макроопределение - директива #define

Макроопределение, или макроподстановка - пожалуй, самая мощная директива препроцессора. И вместе с тем самая коварная. Давайте посмотрим, как ей пользоваться. Взгляните на такой кусочек программы:

int x;
int y;
int i;

for (i=0; i<100; i++) {
x[i]=fx(i);
y[i]=fy(i);
}

Этот код считает значения функций fx и fy в 100 точках - например, для того, чтобы потом построить их графики. Если нам понадобится увеличить количество точек, придется в трех местах заменять 100 на другое число - и писанины много, и ошибиться легко.

45Шаблоны функций.

Шабло́ны (англ. template ) - средство языка C++, предназначенное для кодирования обобщённых алгоритмов, без привязки к некоторым параметрам (например, типам данных, размерам буферов, значениям по умолчанию).

В C++ возможно создание шаблонов функций и классов.

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

Class SomeClass{ int SomeValue; int SomeArray; ...};

Для одной конкретной цели мы можем использовать этот класс. Но, вдруг, цель немного изменилась, и нужен еще один класс. Теперь нужно 30 элементов массива SomeArray и вещественный тип SomeValue элементов SomeArray. Тогда мы можем абстрагироваться от конкретных типов и использовать шаблоны с параметрами. Синтаксис: в начале перед объявлением класса напишем слово template и укажем параметры в угловых скобках. В нашем примере:

Template < int ArrayLength, typename SomeValueType > class SomeClass{ SomeValueType SomeValue; SomeValueType SomeArray[ ArrayLength ]; ...};

Тогда для первой модели пишем:

SomeClass < 20, int > SomeVariable;

для второй:

SomeClass < 30, double > SomeVariable2;

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

46 Шаблоны классов.

Любой шаблон начинается со слова template, будь то шаблон функции или шаблон класса. После ключевого словаtemplate идут угловые скобки - < >, в которых перечисляется список параметров шаблона. Каждому параметру должно предшествовать зарезервированное слово class или typename. Отсутствие этих ключевых слов будет расцениваться компилятором как синтаксическая ошибка. Некоторые примеры объявления шаблонов.

Ключевое слово typename говорит о том, что в шаблоне будет использоваться встроенный тип данных, такой как: int, double,float, char и т. д. А ключевое слово class сообщает компилятору, что в шаблоне функции в качестве параметра будут использоваться пользовательские типы данных, то есть классы. Но не в коем случае не путайте параметр шаблона и шаблон класса. Если нам надо создать шаблон класса, с одним параметром типа int и char, шаблон класса будет выглядеть так.

Как видите шаблон класса Stack объявлен и определен в файле с main-функцией. Конечно же такой способ утилизации шаблонов никуда не годится, но для примера сойдет. В строках 7 - 20 объявлен интерфейс шаблона класса. Объявление класса выполняется привычным для нас образом, а перед классом находится объявление шаблона, в строке 7. При объявлении шаблона класса, всегда используйте такой синтаксис.

Строки 47 - 100 содержат элемент-функции шаблона класса Stack, причем перед каждой функцией необходимо объявлять шаблон, точно такой же, как и перед классом - template . То есть получается, элемент-функции шаблона класса, объявляются точно также, как и обычные шаблоны функций. Если бы мы описали реализацию методов внутри класса, то заголовок шаблона - template для каждой функции прописывать не надо.

Чтобы привязать каждую элемент-функцию к шаблону класса, как обычно используем бинарную операцию разрешения области действия - :: с именем шаблона класса - Stack. Что мы и сделали в строках 49, 58, 68, 83, 96.

Обратите внимание на объявление объекта myStack шаблона класса Stack в функции main, строка 24. В угловых скобочка необходимо явно указывать используемый тип данных, в шаблонах функций этого делать не нужно было. Далее в mainзапускаются некоторые функции, которые демонстрируют работу шаблона класса Stack. Результат работы программы смотрим ниже.

47Библиотека STL. Другие библиотеки контейнерных классов.

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

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

14.1. Инициализация класса

Рассмотрим следующее определение класса:

Class Data { public: int ival; char *ptr; };

Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival - его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.

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

Data dat01("Venus and the Graces", 107925); Data dat02("about"); Data dat03(107925); Data dat04;

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

Обязан ли наш класс Data иметь конструктор? Нет, поскольку все его члены открыты. Унаследованный из языка C механизм поддерживает явную инициализацию, аналогичную используемой при инициализации массивов:

Int main() { // local1.ival = 0; local1.ptr = 0 Data local1 = { 0, 0 }; // local2.ival = 1024; // local3.ptr = "Anna Livia Plurabelle" Data.local2 - { 1024, "Anna Livia Plurabelle" }; // ... }

Значения присваиваются позиционно, на основе порядка, в котором объявляются данные-члены. Следующий пример приводит к ошибке компиляции, так как ival объявлен перед ptr:

// ошибка: ival = "Anna Livia Plurabelle"; // ptr = 1024 Data.local2 - { "Anna Livia Plurabelle", 1024 };

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

Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если он определен как встроенный. Это особенно удобно при работе с глобальными объектами1.

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

14.2. Конструктор класса

Среди других функций-членов конструктор выделяется тем, что его имя совпадает с именем класса. Для объявления конструктора по умолчанию мы пишем2:

Class Account { public: // конструктор по умолчанию... Account(); // ... private: char *_name; unsigned int _acct_nmbr; double _balance; };

Единственное синтаксическое ограничение, налагаемое на конструктор, состоит в том, что он не должен иметь тип возвращаемого значения, даже void. Поэтому следующие объявления ошибочны:

// ошибки: у конструктора не может быть типа возвращаемого значения void Account::Account() { ... } Account* Account::Account(const char *pc) { ... }

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

Откуда мы знаем, сколько и каких конструкторов определить? Как минимум, необходимо присвоить начальное значение каждому члену, который в этом нуждается. Например, номер счета либо задается явно, либо генерируется автоматически таким образом, чтобы гарантировать его уникальность. Предположим, что он будет создаваться автоматически. Тогда мы должны разрешить инициализировать оставшиеся два члена _name и _balance:

Account(const char *name, double open_balance);

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

Account newAcct("Mikey Matz", 0);

Если же есть много счетов, для которых начальный баланс равен 0, то полезно иметь конструктор, задающий только имя владельца и автоматически инициализирующий _balance нулем. Один из способов сделать это - предоставить конструктор вида:

Account(const char *name);

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

Account(const char *name, double open_balance = 0.0);

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

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

Class Account { public: // конструктор по умолчанию... Account(); // имена параметров в объявлении указывать необязательно Account(const char*, double=0.0); const char* name() { return name; } // ... private: // ... };

Ниже приведены два примера правильного определения объекта класса Account, где конструктору передается один или два аргумента:

Int main() { // правильно: в обоих случаях вызывается конструктор // с двумя параметрами Account acct("Ethan Stern"); Account *pact = new Account("Michael Lieberman", 5000); if (strcmp(acct.name(), pact->name())) // ... }

C++ требует, чтобы конструктор применялся к определенному объекту до его первого использования. Это означает, что как для acct, так и для объекта, на который указывает pact, конструктор будет вызван перед проверкой в инструкции if.

Компилятор перестраивает нашу программу, вставляя вызовы конструкторов. Вот как, по всей вероятности, будет модифицировано определение acct внутри main():

// псевдокод на C++, // иллюстрирующий внутреннюю вставку конструктора int main() { Account acct; acct.Account::Account("Ethan Stern", 0.0); // ... }

Конечно, если конструктор определен как встроенный, то он подставляется в точке вызова.

Обработка оператора new несколько сложнее. Конструктор вызывается только тогда, когда он успешно выделил память. Модификация определения pact в несколько упрощенном виде выглядит так:

// псевдокод на C++, // иллюстрирующий внутреннюю вставку конструктора при обработке new int main() { // ... Account *pact; try { pact = _new(sizeof(Account)); pact->Acct.Account::Account("Michael Liebarman", 5000.0); } catch(std::bad_alloc) { // оператор new закончился неудачей: // конструктор не вызывается } // ... }

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

// в общем случае эти формы эквивалентны Account acct1("Anna Press"); Account acct2 = Account("Anna Press"); Account acct3 = "Anna Press";

Форма acct3 может использоваться только при задании единственного аргумента. Если аргументов два или более, мы рекомендуем пользоваться формой acct1, хотя допустима и acct2.

Новички часто допускают ошибку при объявлении объекта, инициализированного конструктором по умолчанию:

// увы! работает не так, как ожидалось Account newAccount();

Эта инструкция компилируется без ошибок. Однако при попытке использовать объект в таком контексте:

// ошибка компиляции... if (! newAccount.name()) ...

компилятор не сможет применить к функции нотацию доступа к членам класса. Определение

// определяет функцию newAccount, // а не объект класса Account newAccount();

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

// правильно: определяется объект класса... Account newAccount;

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

Class Account { public: // имена параметров в объявлении указывать необязательно Account(const char*, double=0.0); const char* name() { return name; } // ... private: // ... };

Теперь при объявлении каждого объекта Account в конструкторе обязательно надо указать как минимум аргумент типа C-строки, но это скорее всего бессмысленно. Почему? Контейнерные классы (например, vector) требуют, чтобы для класса помещаемых в них элементов был либо задан конструктор по умолчанию, либо вообще никаких конструкторов. Аналогичная ситуация имеет место при выделении динамического массива объектов класса. Так, следующая инструкция вызвала бы ошибку компиляции для новой версии Account:

// ошибка: требуется конструктор по умолчанию для класса Account Account *pact = new Account[ new_client_cnt ];

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

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

// конструктор по умолчанию для класса Account inline Account:: Account() { _name = 0; _balance = 0.0; _acct_nmbr = 0; }

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

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

// конструктор по умолчанию класса Account с использованием // списка инициализации членов inline Account:: Account() : _name(0), _balance(0.0), _acct_nmbr(0) {}

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

Inline Account:: Account(const char* name, double opening_bal) : _balance(opening_bal) { _name = new char[ strlen(name)+1 ]; strcpy(_name, name); _acct_nmbr = get_unique_acct_nmbr(); }

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

Конструктор нельзя объявлять с ключевыми словами const или volatile (см. раздел 13.3.5), поэтому приведенные записи неверны:

Class Account { public: Account() const; // ошибка Account() volatile; // ошибка // ... };

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

Рассмотрим следующий фрагмент программы:

// в каком-то заголовочном файле extern void print(const Account &acct); // ... int main() { // преобразует строку "oops" в объект класса Account // с помощью конструктора Account::Account("oops", 0.0) print("oops"); // ... }

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

Непреднамеренные неявные преобразования классов, например трансформация "oops" в объект класса Account, оказались источником трудно обнаруживаемых ошибок. Поэтому в стандарт C++ было добавлено ключевое слово explicit, говорящее компилятору, что такие преобразования не нужны:

Class Account { public: explicit Account(const char*, double=0.0); };

Данный модификатор применим только к конструктору. (Операторы преобразования и слово explicit обсуждаются в разделе 15.9.2 .)

14.2.1. Конструктор по умолчанию

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

// все это конструкторы по умолчанию Account::Account() { ... } iStack::iStack(int size = 0) { ... } Complex::Complex(double re=0.0, double im=0.0) { ... }

Когда мы пишем:

Int main() { Account acct; // ... }

то компилятор сначала проверяет, определен ли для класса Account конструктор по умолчанию. Возникает одна из следующих ситуаций:

  1. Такой конструктор определен. Тогда он применяется к acct.
  2. Конструктор определен, но не является открытым. В данном случае определение acct помечается компилятором как ошибка: у функции main() нет прав доступа.
  3. Конструктор по умолчанию не определен, но есть один или несколько конструкторов, требующих задания аргументов. Определение acct помечается как ошибка: слишком мало аргументов у конструктора.
  4. Нет ни конструктора по умолчанию, ни какого-либо другого. Определение считается корректным, acct не инициализируется, конструктор не вызывается.

Пункты 1 и 3 должны быть уже достаточно понятны (если это не так, перечитайте данную главу) Посмотрим более внимательно на пункты 2 и 4.

Допустим, что все члены класса Account объявлены открытыми и не объявлено никакого конструктора:

Class Account { public: char *_name; unsigned int _acct_nmbr; double _balance; };

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

// статический класс хранения // вся ассоциированная с объектом память обнуляется Account global_scope_acct; static Account file_scope_acct; Account foo() { static Account local_static_acct; // ... }

Однако объекты, определенные локально или распределенные динамически, в начальный момент будут содержать случайный набор битов, оставшихся в стеке программы:

// локальные и распределенные из хипа объекты не инициализированы // до момента явной инициализации или присваивания Account bar() { Account local_acct; Account *heap_acct = new Account; // ... }

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

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

14.2.2. Ограничение прав на создание объекта

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

Class Account { friend class vector; public: explicit Account(const char*, double = 0.0); // ... private: Account(); // ... };

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

Конструкторы, не являющиеся открытыми, в реальных программах C++ чаще всего используются для:

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

14.2.3. Копирующий конструктор

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

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

Копирующий конструктор принимает в качестве формального параметра ссылку на объект класса (традиционно объявляемую со спецификатором const). Вот его реализация:

Inline Account:: Account(const Account &rhs) : _balance(rhs._balance) { _name = new char[ strlen(rhs._name) + 1 ]; strcpy(_name, rhs._name); // копировать rhs._acct_nmbr нельзя _acct_nmbr = get_unique_acct_nmbr(); }

Когда мы пишем:

Account acct2(acct1);

компилятор определяет, объявлен ли явный копирующий конструктор для класса Account. Если он объявлен и доступен, то он и вызывается; а если недоступен, то определение acct2 считается ошибкой. В случае, когда копирующий конструктор не объявлен, выполняется почленная инициализация по умолчанию. Если впоследствии объявление копирующего конструктора будет добавлено или удалено, никаких изменений в программы пользователей вносить не придется. Однако перекомпилировать их все же необходимо. (Более подробно почленная инициализация рассматривается в разделе 14.6.)

Упражнение 14.1

Какие из следующих утверждений ложны? Почему?

  1. У класса должен быть хотя бы один конструктор.
  2. Конструктор по умолчанию - это конструктор с пустым списком параметров.
  3. Если разумных начальных значений у членов класса нет, то не следует предоставлять конструктор по умолчанию.
  4. Если в классе нет конструктора по умолчанию, то компилятор генерирует его автоматически и инициализирует каждый член значением по умолчанию для соответствующего типа.

Упражнение 14.2

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

Class NoName { public: // здесь должны быть конструкторы // ... protected: char *pstring; int ival; double dval; };

Упражнение 14.3

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

  1. Книга
  2. Служащий
  3. Транспортное средство
  4. Объект
  5. Дерево

Упражнение 14.4

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

Class Account { public: Account(); explicit Account(const char*, double=0.0); // ... };

объясните, что происходит в результате следующих определений:

(a) Account acct; (b) Account acct2 = acct; (c) Account acct3 = "Rena Stern "; (d) Account acct4("Anna Engel ", 400.00); (e) Account acct5 = Account(acct3);

Упражнение 14.5

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

Account::Account(const Account rhs);

14.3. Деструктор класса

Одна из целей, стоящих перед конструктором, - обеспечить автоматическое выделение ресурса. Мы уже видели в примере с классом Account конструктор, где с помощью оператора new выделяется память для массива символов и присваивается уникальный номер счету. Можно также представить ситуацию, когда нужно получить монопольный доступ к разделяемой памяти или к критической секции потока. Для этого необходима симметричная операция, обеспечивающая автоматическое освобождение памяти или возврат ресурса по завершении времени жизни объекта, - деструктор. Деструктор - это специальная определяемая пользователем функция-член, которая автоматически вызывается, когда объект выходит из области видимости или когда к указателю на объект применяется операция delete. Имя этой функции образовано из имени класса с предшествующим символом "тильда" (~). Деструктор не возвращает значения и не принимает никаких параметров, а следовательно, не может быть перегружен. Хотя разрешается определять несколько таких функций-членов, лишь одна из них будет применяться ко всем объектам класса. Вот, например, деструктор для нашего класса Account:

Class Account { public: Account(); explicit Account(const char*, double=0.0); Account(const Account&); ~Account(); // ... private: char *_name; unsigned int _acct_nmbr; double _balance; }; inline Account::~Account() { delete _name; return_acct_number(_acct_nnmbr); }

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

Inline Account::~Account() { // необходимо delete _name; return_acct_number(_acct_nnmbr); // необязательно _name = 0; _balance = 0.0; _acct_nmbr = 0; }

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

Class Point3d { public: // ... private: float x, y, z; };

Конструктор здесь необходим для инициализации членов, представляющих координаты точки. Нужен ли деструктор? Нет. Для объекта класса Point3d не требуется освобождать ресурсы: память выделяется и освобождается компилятором автоматически в начале и в конце его жизни.

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

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

{ // начало критического участка программы #ifdef PROFILE Timer t; #endif // критический участок // t уничтожается автоматически // отображается затраченное время... }

Чтобы убедиться в том, что мы понимаем поведение деструктора (да и конструктора тоже), разберем следующий пример:

(1) #include "Account.h" (2) Account global("James Joyce"); (3) int main() (4) { (5) Account local("Anna Livia Plurabelle", 10000); (6) Account &loc_ref = global; (7) Account *pact = 0; (8) (9) { (10) Account local_too("Stephen Hero"); (11) pact = new Account("Stephen Dedalus"); (12) } (13) (14) delete pact; (15) }

Сколько здесь вызывается конструкторов? Четыре: один для глобального объекта global в строке (2); по одному для каждого из локальных объектов local и local_too в строках (5) и (10) соответственно, и один для объекта, распределенного в хипе, в строке (11). Ни объявление ссылки loc_ref на объект в строке (6), ни объявление указателя pact в строке (7) не приводят к вызову конструктора. Ссылка - это псевдоним для уже сконструированного объекта, в данном случае для global. Указатель также лишь адресует объект, созданный ранее (в данном случае распределенный в хипе, строка (11)), или не адресует никакого объекта (строка (7)).

Аналогично вызываются четыре деструктора: для глобального объекта global, объявленного в строке (2), для двух локальных объектов и для объекта в хипе при вызове delete в строке (14). Однако в программе нет инструкции, с которой можно связать вызов деструктора. Компилятор просто вставляет эти вызовы за последним использованием объекта, но перед закрытием соответствующей области видимости.

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

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

С++ с помощью внутренних механизмов препятствует применению оператора delete к указателю, не адресующему никакого объекта, так что соответствующие проверки кода необязательны:

// необязательно: неявно выполняется компилятором if (pact != 0) delete pact;

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

#include #include "Account.h" Account global("James Joyce"); int main() { Account local("Anna Livia Plurabelle", 10000); Account &loc_ref = global; auto_ptr pact(new Account("Stephen Dedalus")); { Account local_too("Stephen Hero"); } // объект auto_ptr уничтожается здесь }

14.3.1. Явный вызов деструктора

Иногда вызывать деструктор для некоторого объекта приходится явно. Особенно часто такая необходимость возникает в связи с оператором new (см. раздел 8.4). Рассмотрим пример. Когда мы пишем:

Char *arena = new char[ sizeof Image ];

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

Image *ptr = new (arena) Image("Quasimodo");

то никакой новой памяти не выделяется. Вместо этого переменной ptr присваивается адрес, ассоциированный с переменной arena. Теперь память, на которую указывает ptr, интерпретируется как занимаемая объектом класса Image, и конструктор применяется к уже существующей области. Таким образом, оператор размещения new() позволяет сконструировать объект в ранее выделенной области памяти.

Закончив работать с изображением Quasimodo, мы можем произвести какие-то операции с изображением Esmerelda, размещенным по тому же адресу arena в памяти:

Image *ptr = new (arena) Image("Esmerelda");

Однако изображение Quasimodo при этом будет затерто, а мы его модифицировали и хотели бы записать на диск. Обычно сохранение выполняется в деструкторе класса Image, но если мы применим оператор delete:

// плохо: не только вызывает деструктор, но и освобождает память delete ptr;

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

Ptr->~Image();

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

Отметим, что, хотя ptr и arena адресуют одну и ту же область памяти в хипе, применение оператора delete к arena

// деструктор не вызывается delete arena;

не приводит к вызову деструктора класса Image, так как arena имеет тип char*, а компилятор вызывает деструктор только тогда, когда операндом в delete является указатель на объект класса, имеющего деструктор.

14.3.2. Опасность увеличения размера программы

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

Account acct("Tina Lee"); int swt; // ... switch(swt) { case 0: return; case 1: // что-то сделать return; case 2: // сделать что-то другое return; // и так далее }

компилятор подставит деструктор перед каждой инструкцией return. Деструктор класса Account невелик, и затраты времени и памяти на его подстановку тоже малы. В противном случае придется либо объявить деструктор невстроенным, либо реорганизовать программу. В примере выше инструкцию return в каждой метке case можно заменить инструкцией break с тем, чтобы у функции была единственная точка выхода:

// переписано для обеспечения единственной точки выхода switch(swt) { case 0: break; case 1: // что-то сделать break; case 2: // сделать что-то другое break; // и так далее } // единственная точка выхода return;

Упражнение 14.6

Напишите подходящий деструктор для приведенного набора членов класса, среди которых pstring адресует динамически выделенный массив символов:

Class NoName { public: ~NoName(); // ... private: char *pstring; int ival; double dval; };

Упражнение 14.7

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

Упражнение 14.8

Сколько раз вызываются деструкторы в следующем фрагменте:

Void mumble(const char *name, fouble balance, char acct_type) { Account acct; if (! name) return; if (balance <= 99) return; switch(acct_type) { case "z": return; case "a": case "b": return; } // ... }

14.4. Массивы и векторы объектов

Массив объектов класса определяется точно так же, как массив элементов встроенного типа. Например:

Account table[ 16 ];

определяет массив из 16 объектов Account. Каждый элемент по очереди инициализируется конструктором по умолчанию. Можно и явно передать конструкторам аргументы внутри заключенного в фигурные скобки списка инициализации массива. Строка:

Account pooh_pals = { "Piglet", "Eeyore", "Tigger" };

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

Account("Piglet", 0.0); // первый элемент (Пятачок) Account("Eeyore", 0.0); // второй элемент (Иа-Иа) Account("Tigger", 0.0); // третий элемент (Тигра)

Один аргумент можно задать явно, как в примере выше. Если же необходимо передать несколько аргументов, то придется воспользоваться явным вызовом конструктора:

Account pooh_pals = { Account("Piglet", 1000.0), Account("Eeyore", 1000.0), Account("Tigger", 1000.0) };

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

Account pooh_pals = { Account("Woozle", 10.0), // Бука Account("Heffalump", 10.0), // Слонопотам Account(); };

Эквивалентный массив из трех элементов можно объявить и так:

Account pooh_pals = { Account("Woozle", 10.0), Account("Heffalump", 10.0) };

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

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

Pooh_pals; обращается к Piglet, а pooh_pals;

к Eeyore и т.д. Для доступа к членам объекта, находящегося в некотором элементе массива, мы сочетаем операторы взятия индекса и доступа к членам:

Pooh_pals._name != pooh_pals._name;

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

Объявление

Account *pact = new Account[ 10 ];

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

Чтобы уничтожить массив, адресованный указателем pact, необходимо применить оператор delete. Однако написать

// увы! это не совсем правильно delete pact;

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

// правильно: // показывает, что pact адресует массив delete pact;

Пустая пара скобок говорит о том, что pact адресует именно массив. Компилятор определяет, сколько в нем элементов, и применяет деструктор к каждому из них.

14.4.1. Инициализация массива, распределенного из хипа A

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

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

#include #include #include #include #include "Accounts.h" typedef pair value_pair; /* init_heap_array() * объявлена как статическая функция-член * обеспечивает выделение памяти из хипа и инициализацию * массива объектов * init_values: пары начальных значений элементов массива * elem_count: число элементов в массиве * если 0, то размером массива считается размер вектора * init_values */ Account* Account:: init_heap_array(vector &init_values, vector ::size_type elem_count = 0) { vector ::size_type vec_size = init_value.size(); if (vec_size == 0 && elem_count == 0) return 0; // размер массива равен либо elem_count, // либо, если elem_count == 0, размеру вектора... size_t elems = elem_count ? elem_count: vec_size(); // получить блок памяти для размещения массива char *p = new char; // по отдельности инициализировать каждый элемент массива int offset = sizeof(Account); for (int ix = 0; ix < elems; ++ix) { // смещение ix-ого элемента // если пара начальных значений задана, // передать ее конструктору; // в противном случае вызвать конструктор по умолчанию if (ix < vec_size) new(p+offset*ix) Account(init_values.first, init_values.second); else new(p+offset*ix) Account; } // отлично: элементы распределены и инициализированы; // вернуть указатель на первый элемент return (Account*)p; }

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

Char *p = new char;

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

For (int ix = 0; ix

В разделе 14.3 говорилось, что оператор размещения new позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete работать не будет:

Delete ps;

Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору неизвестно. Так что всю работу придется сделать самим:

Void Account:: dealloc_heap_array(Account *ps, size_t elems) { for (int ix = 0; ix < elems; ++ix) ps.Account::~Account(); delete reinterpret_cast(ps); }

Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:

New(p+offset*ix) Account;

то здесь мы обращаемся к ним, задавая индекс в массиве ps:

Ps.Account::~Account();

Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p - как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p ассоциирован не тот тип, что нужно, арифметические операции над указателями приходится программировать самостоятельно.

Мы объявляем обе функции статическими членами класса:

Typedef pair value_pair; class Account { public: // ... static Account* init_heap_array(vector< value_pair> &init_values, vector< value_pair> ::size_type elem_count = 0); static void dealloc_heap_array(Account*, size_t); // ... };

14.4.2. Вектор объектов

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

Vector< Point > vec(5);

то инициализация элементов производится в следующем порядке5:

  1. С помощью конструктора по умолчанию создается временный объект типа класса, хранящегося в векторе.
  2. К каждому элементу вектора применяется копирующий конструктор, в результате чего каждый объект инициализируется копией временного объекта.
  3. Временный объект уничтожается.

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

Point pa[ 5 ];

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

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

Vector< Point > cvs; // пустой int cv_cnt = calc_control_vertices(); // зарезервировать память для хранения cv_cnt объектов класса Point // cvs все еще пуст... cvs.reserve(cv_cnt); // открыть файл и подготовиться к чтению из него ifstream infile("spriteModel"); istream_iterator< Point> cvfile(infile),eos; // вот теперь можно вставлять элементы copy(cvfile, eos, inserter(cvs, cvs.begin()));

(Алгоритм copy(), итератор вставки inserter и потоковый итератор чтения istream_iterator рассматривались в главе 12 .) Поведение объектов list (список) и deque (двусторонняя очередь) аналогично поведению объектов vector (векторов). Вставка объекта в любой из этих контейнеров осуществляется с помощью копирующего конструктора.

Упражнение 14.9

Какие из приведенных инструкций неверны? Исправьте их.

(a) Account *parray = new Account; (b) Account iA = { "Nhi", "Le", "Jon", "Mike", "Greg", "Brent", "Hank" "Roy", "Elena" }; (c) string *ps=string("Tina","Tim","Chyuan","Mira","Mike"); (d) string as = *ps; Упражнение 14.10

Что лучше применить в каждой из следующих ситуаций: статический массив (такой, как Account pA), динамический массив или вектор? Объясните свой выбор.

Внутри функции Lut() нужен набор из 256 элементов для хранения объектов класса Color. Значения являются константами.

Необходимо хранить набор из неизвестного числа объектов класса Account. Данные счетов читаются из файла.

Функция gen_words(elem_size) должна сгенерировать и передать обработчику текста набор из elem_size строк.

Упражнение 14.11

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

// печально: не проверяется, что parray адресует массив delete parray; вместо // правильно: определяется размер массива, адресуемого parray delete parray;

Наличие пары скобок заставляет компилятор найти размер массива. Затем к каждому элементу по очереди применяется деструктор (всего size раз). Если же скобок нет, уничтожается только один элемент. В любом случае освобождается вся память, занятая массивом.

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

// в первоначальном варианте языка размер массива требовалось задавать явно delete p parray;

Как вы думаете, почему язык был изменен таким образом, что явного задания размера не требуется (а значит, нужно уметь его сохранять и извлекать), но скобки, хотя и пустые, в операторе delete остались (так что компилятор не должен запоминать, адресует указатель единственный объект или массив)? Какой вариант языка предложили бы вы?

14.5. Список инициализации членов

Модифицируем наш класс Account, объявив член _name типа string:

#include >string> class Account { public: // ... private: unsigned int _acct_nmbr; double _balance; string _name; };

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

Исходный конструктор Account с двумя параметрами

Account(const char*, double = 0.0);

не может инициализировать член типа string. Например:

String new_client(" Steve Hall"); Account new_acct(new_client, 25000);

не будет компилироваться, так как не существует неявного преобразования из типа string в тип char*. Инструкция

Account new_acct(new_client.c_str(), 25000);

правильна, но вызовет у пользователей класса недоумение. Одно из решений - добавить новый конструктор вида:

Account(string, double = 0.0);

Если написать:

Account new_acct(new_client, 25000);

вызывается именно этот конструктор, тогда как старый код

Account *open_new_account(const char *nm) { Account *pact = new Account(nm); // ... return pacct; }

по-прежнему будет приводить к вызову исходного конструктора с двумя параметрами.

Так как в классе string определено преобразование из типа char* в тип string (преобразования классов обсуждаются в этой главе ниже), то можно заменить исходный конструктор на новый, которому в качестве первого параметра передается тип string. В таком случае, когда встречается инструкция:

Account myAcct(" Tinkerbell");

" Tinkerbell" преобразуется во временный объект типа string. Затем этот объект передается новому конструктору с двумя параметрами.

При проектировании приходится идти на компромисс между увеличением числа конструкторов класса Account и несколько менее эффективной обработкой аргументов типа char* из-за необходимости создавать временный объект. Мы предоставили две версии конструктора с двумя параметрами. Тогда модифицированный набор конструкторов Account будет таким:

#include class Account { public: Account(); Account(const char*, double=0.0); Account(const string&, double=0.0); Account(const Account&); // ... private: // ... };

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

  1. где вызывается конструктор по умолчанию? Внутри конструктора по умолчанию класса Account;
  2. где вызывается копирующий конструктор? Внутри копирующего конструктора класса Account и внутри конструктора с двумя параметрами, принимающего в качестве первого тип string;
  3. как передать аргументы конструктору класса, являющегося членом другого класса? Это необходимо делать внутри конструктора Account с двумя параметрами, принимающего в качестве первого тип char*.

Решение заключается в использовании списка инициализации членов (мы упоминали о нем в разделе 14.2). Члены, являющиеся классами, можно явно инициализировать с помощью списка, состоящего из разделенных запятыми пар "имя члена/значение". Наш конструктор с двумя параметрами теперь выглядит так (напомним, что _name - это член, являющийся объектом класса string):

Список инициализации членов следует за сигнатурой конструктора и отделяется от нее двоеточием. В нем указывается имя члена, а в скобках - начальные значения, что аналогично синтаксису вызова функции. Если член является объектом класса, то эти значения становятся аргументами, передаваемыми подходящему конструктору, который затем и используется. В нашем примере значение name передается конструктору string, который применяется к члену _name. Член _balance инициализируется значением opening_bal.

Аналогично выглядит второй конструктор с двумя параметрами:

Inline Account:: Account(const string& name, double opening_bal) : _name(name), _balance(opening_bal) { _acct_nmbr = het_unique_acct_nmbr(); }

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

Часто у новичков возникает вопрос: в чем разница между использованием списка инициализации и присваиванием значений членам в теле конструктора? Например, в чем разница между

Inline Account:: Account(const char* name, double opening_bal) : _name(name), _balance(opening_bal) { _acct_nmbr = het_unique_acct_nmbr(); }

Account(const char* name, double opening_bal) { _name = name; _balance = opening_bal; _acct_nmbr = het_unique_acct_nmbr(); }

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

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

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

Inline Account:: Account() { _name = ""; _balance = 0.0; _acct_nmbr = 0; }

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

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

Inline Account:: Account() : _name(string()) { _balance = 0.0; _acct_nmbr = 0; }

Мы удалили ненужное присваивание _name из тела конструктора. Явный же вызов конструктора по умолчанию string излишен. Ниже приведена эквивалентная, но более компактная версия:

Inline Account:: Account() { _balance = 0.0; _acct_nmbr = 0; }

Однако мы еще не ответили на вопрос об инициализации двух членов встроенных типов. Например, так ли существенно, где происходит инициализация _balance: в списке инициализации или в теле конструктора? Инициализация и присваивание членам, не являющимся объектами классов, эквивалентны как с точки зрения результата, так и с точки зрения производительности (за двумя исключениями). Мы предпочитаем использовать список:

// предпочтительный стиль инициализации inline Account:: Account() : _balance(0.0), _acct_nmbr(0) {}

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

Class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; }; ConstRef:: ConstRef(int ii) { // присваивание i = ii; // правильно ci = ii; // ошибка: нельзя присваивать константному члену ri = i; // ошибка: ri не инициализирована }

К началу выполнения тела конструктора инициализация всех константных членов и членов-ссылок должна быть завершена. Для этого нужно указать их в списке инициализации. Правильная реализация предыдущего примера такова:

// правильно: инициализируются константные члены и ссылки ConstRef:: ConstRef(int ii) : ci(ii), ri (i) { i = ii; }

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

Class Account { public: // ... private: unsigned int _acct_nmbr; double _balance; string _name; };

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

Inline Account:: Account() : _name(string()), _balance(0.0), _acct_nmbr(0) {} будет следующим: _acct_nmbr, _balance, _name. Однако члены, указанные в списке (или в неявно инициализируемом члене-объекте класса), всегда инициализируются раньше, чем производится присваивание членам в теле конструктора. Например, в следующем конструкторе: inline Account:: Account(const char* name, double bal) : _name(name), _balance(bal) { _acct_nmbr = get_unique_acct_nmbr(); }

порядок инициализации такой: _balance, _name, _acct_nmbr.

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

Class X { int i; int j; public: // видите проблему? X(int val) : j(val), i(j) {} // ... };

кажется, что перед использованием для инициализации i член j уже инициализирован значением val, но на самом деле i инициализируется первым, для чего применяется еще неинициализированный член j. Мы рекомендуем помещать инициализацию одного члена другим (если вы считаете это необходимым) в тело конструктора:

// предпочтительная идиома X::X(int val) : i(val) { j = i; }

Упражнение 14.12

Что неверно в следующих определениях конструкторов? Как бы вы исправили обнаруженные ошибки?

(a) Word::Word(char *ps, int count = 1) : _ps(new char), _count(count) { if (ps) strcpy(_ps, ps); else { _ps = 0; _count = 0; } } (b) class CL1 { public: CL1() { c.real(0.0); c.imag(0.0); s = " not set" ; } // ... private: complex c; string s; } (c) class CL2 { public: CL2(map *pmap, string key) : _text(key), _loc((*pmap)) {} // ... private: location _loc; string _text; };

14.6. Почленная инициализация A

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

Account oldAcct(" Anna Livia Plurabelle"); Account newAcct(oldAcct);

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

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

Class Account { public: // ... private: char *_name; unsigned int _acct_nmbr; double _balance; };

то можно представить, что копирующий конструктор по умолчанию определен так:

Inline Account:: Account(const Account &rhs) { _name = rhs._name; _acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance; }

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

1. явная инициализация одного объекта другим:

Account newAcct(oldAcct);

1. передача объекта класса в качестве аргумента функции:

Extern bool cash_on_hand(Account acct); if (cash_on_hand(oldAcct)) // ...

1. передача объекта класса в качестве возвращаемого функцией значения:

Extern Account consolidate_accts(const vector< Account > &) { Account final_acct; // выполнить финансовую операцию return final_acct; }

1. определение непустого последовательного контейнера:

// вызывается пять копирующих конструкторов класса string vector< string > svec(5);

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

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

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

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

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

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

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

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, все равно нужно). Язык не дает нам возможности ограничить доступ к закрытым членам класса со стороны функций-членов и друзей. Но если определение отсутствует, то любая попытка вызвать копирующий конструктор, законная с точки зрения компилятора, приведет к ошибке во время редактирования связей, поскольку не удастся найти определение символа.

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

Class Account { public: Account(); Account(const char*, double=0.0); // ... private: Account(const Account&); // ... };

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

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

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

NewAcct._name = oldAcct._name;

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

Account newAcct(oldAcct);

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

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

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

Inline Account:: Account(const Account &rhs) { _acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance; // Псевдокод на C++ // иллюстрирует вызов копирующего конструктора // для члена, являющегося объектом класса _name.string::string(rhs._name); }

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

// не совсем правильно... inline Account:: Account(const Account &rhs) { _name = rhs._name; _balance = rhs._balance; _acct_nmbr = get_unique_acct_nmbr(); }

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

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; }; (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;

по умолчанию присваивает каждому нестатическому члену 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:

Account& Account:: operator=(const Account &rhs) { // не надо присваивать самому себе if (this != &rhs) { delete _name; _name = new char; 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); }

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

Account& Account:: operator=(const Account &rhs) { // не надо присваивать самому себе if (this != &rhs) { // вызывается string::operator=(const string&) _name = rhs._name; _balance = rhs._balance; } return *this; }

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

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

Упражнение 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);

то достаточно скопировать адрес объекта 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 не определено после выхода из функции, в которой она объявлена. (Мы возвращаем ссылку на локальный объект, который после возврата не существует.)

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

// нет возможности гарантировать отсутствие утечки памяти // поскольку матрица может быть большой, утечки будут весьма заметными 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) ...

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

Matrix& operator+(const Matrix& m1, const Matrix& m2) name result { Matrix result; // ... return result; }

Тогда компилятор мог бы самостоятельно переписать функцию, добавив к ней третий параметр-ссылку:

// переписанная компилятором функция // в случае принятия предлагавшегося расширения языка void operator+(Matrix &result, const Matrix& m1, const Matrix& m2) name result { // вычислить результат }

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

Matrix c = a + b;

было бы трансформировано в

Matrix c; operator+(c, a, b);

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

ClassType functionName(paramList) { classType namedResult; // выполнить какие-то действия... return namedResult; }

то компилятор самостоятельно трансформирует как саму функцию, так и все обращения к ней:

Void functionName(classType &namedResult, paramList) { // вычислить результат и разместить его по адресу namedResult }

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

И последнее замечание об эффективности работы с объектами в C++. Инициализация объекта класса вида

Matrix c = a + b;

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

Matrix c; c = a + b;

но объем требуемых вычислений значительно больше. Аналогично эффективнее писать:

For (int ix = 0; ix

Matrix matSum; for (int ix = 0; ix

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

Point3d p3 = operator+(p1, p2);

можно безопасно трансформировать:

// Псевдокод на C++ Point3d p3; operator+(p3, p1, p2);

преобразование

Point3d p3; p3 = operator+(p1, p2);

// Псевдокод на C++ // небезопасно в случае присваивания operator+(p3, p1, p2); небезопасно.

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

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

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

Point3d p3; p3 = operator+(p1, p2);

трансформируется в такой:

// Псевдокод на C++ Point3d temp; operator+(temp, p1, p2); p3.Point3d::operator=(temp); temp.Point3d::~Point3d();

Майкл Тиманн (Michael Tiemann), автор компилятора GNU C++, предложил назвать это расширение языка именованным возвращаемым значением (return value language extension). Его точка зрения изложена в работе . В нашей книге “Inside the C++ Object Model” () приводится детальное обсуждение затронутых в этой главе тем.

Страница 74 из 82

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

Список параметров в определении и прототипе функции, кроме согласования типов параметров, имеет ещё одно назначение.

Объявление параметра может содержать инициализатор, то есть выражение, которое должно обеспечить параметру присвоение начального значения. Инициализатор параметра не является константным выражением. Начальная инициализация параметров происходит не на стадии компиляции (как, например, выделение памяти под массивы), а непосредственно в ходе выполнения программы.

Следующие строки демонстрируют пример объявления функции с инициализацией параметров. Для инициализации параметра ww используется функция XX.

Int BigVal;
int XX(int);
int ZZ(int tt, int ww = XX(BigVal));

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

Int ZZ(int tt, int = XX(BigVal));

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

Прототипы функции могут располагаться в различных областях видимости. Его можно даже разместить в теле определяемой функции. Каждое объявление функции может содержать собственные варианты объявления и инициализации параметров. Но во множестве объявлений одной и той же функции в пределах одной области видимости не допускается повторная инициализация параметров. Всему должен быть положен разумный предел.

Кроме того, в C++ действует ещё одно ограничение, связанное с порядком инициализации параметров в пределах области видимости. Инициализация проводится непременно с самого последнего (самого правого) параметра в списке объявлений параметров. Инициализация параметров не допускает пропусков: инициализированные параметры не могут чередоваться с параметрами неинициализированными.

Int MyF1 (int par1, int par2, int par3, int par4 = 10);
int MyF1 (int par1, int par2 = 20, int par3 = 20, int par4);
int MyF1 (int par1 = 100, int, int, int);

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

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

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

Инициализация - это что такое?

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

Примеры

Давайте рассмотрим, что представляет собой инициализация. Это что такое? Понять будет легче всего, используя несколько реальных примеров:

  1. Под инициализацией магнитного диска винчестера понимают его форматирование. Также может быть сюда отнесена и запись управляющей информацией (меток томов, описателей дорожек и подобные манипуляции).
  2. Под инициализацией программы понимают задание необходимых переменных или установку в нулевое значение программных переменных (счётчиков, адресов, переключателей, указателей) перед тем, как само приложение будет выполнено. Всё устанавливается согласно тому, что делается для этого типа ПО или содержится в виде указаний в самом файле.
  3. Инициализация для вывода на принтер данных подсистемы печати. Это определение, которое подразумевает следующее: на каком устройстве необходимо перенести данные на бумагу и провести учёт всех необходимостей. Так, следует определить формат печати, расширение, использовать ли цвет (если он есть) и другие параметры. Сначала на устройство подаётся управляющий сигнал и проводится сканирование его возможностей, чтобы произвести первоначальную настройку принтера и предложить пользователю вариант, который может быть осуществлён имеющейся техникой.

А вот теперь давайте рассмотрим случай на практике запуска приложения. Допустим, существует ошибка инициализации при запуске игры. В качестве базы было выбрано Симс 3 - довольно популярное приложение, в котором требуется управлять человеком, создавая ему обеспеченную жизнь.

Из-за чего возникает в Sims-3

Самый распространённый вариант проблемы - это 0×0175dcbb. Этот номер используется для обозначения именно неполадок с инициализацией приложения. Она может возникнуть из-за того, что базовая игра конфликтует с дополнениями, модами и драйверами видеокарты, которые были установлены на компьютере. Причём вы не застрахованы даже при покупке лицензионной игры - если проблема есть, лицензия не приведёт к её автоматическому решению.

Что же делать, если ошибка 0×0175dcbb уже есть?

Итак, ошибка возникла. Сейчас разберёмся, как мы можем её убрать. Хотя рассматривать будем игру, но многие знания, написанные далее, могут пригодиться и при работе с серьёзными приложениями. Помните, что самая старая проблема - это архаические программные компоненты. В первую очередь нас интересуют драйвера видеокарт. Их можно скачать на сайтах производителей, что мы собственно и сделали. Также нелишним будет установить или обновить до последней версии NET Framework - желательно делать это с ресурса разработчика, коим является Microsoft.

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

Заключение

Что устанавливать на свой компьютер, решать только вам. Но впредь, прежде чем добавлять в стабильно работающий продукт контент, разработанный сторонними людьми, необходимо хорошо подумать и проявить значительную внимательность к качеству скачанных дополнений. Потому что может быть ошибка инициализации обновления, а возможность отката обычно не предусматривается. И тогда придётся удалять всю программу и устанавливать её заново. Если уж решено было так сделать, то необходимо позаботится о сохранности наработанных данных. Их копию нужно разместить где-то в безопасном и укромном месте, например, на рабочем столе или флеш-накопителе. Вот и всё, нами была детально рассмотрена инициализация. Это что такое, вы наверняка поняли. Ведь мы разобрали не только текстовую составляющую, но и рассмотрели несколько конкретных примеров.

Классификация указателей на функцию

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

Указатели на функцию

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

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

В языке Си существуют две разновидности указателей на функцию:

· указатель – выражение,

· указатель переменная.

38.9.3. Указатель – выражение

Как было отмечено выше, указателем выражение является выражение, значением которого является адрес функции. Приведем пример.

Пусть имеется прототип функции для вычисления квадрата числа типа double. Тогда выражение &sqr - это выражение – указатель. Здесь & - унарный оператор взятия адреса, который и предписывает компилятору вычислить адрес йункции.

38.9.4. Указатель – переменная

Указатель - переменная это переменная, предназначенная для хранения адреса функции. Рассмотрим формат определения такой переменной.

(* <α>) (<γ>)

· * - оператор разыменования,

α – идентификатор, объявленного указателя,

· b - тип значения, возвращаемого функцией, на которую может быть установлен указатель.

· γ – список типов параметров функции, на которую может быть установлен указатель.

Приведем пример объявления указателя – переменной.

double (*pf)(double);

Это объявление переменной. Имя переменной pf. значением этой переменной может служить адрес любой функции, которая принимает один параметр типа double и которая возращает значения типа double.

Учитывая изложенное выше, можно сказать, что определение указателя на функцию задает множество функций, с которыми он может использоваться. Указатель – переменная pf, объявленная выше, может работать, например, со следующими функциями.

double sqr(double x)
{

double cube(double x)
{

return x * x * x;

Вернемся к объявлению указателя – переменной pf. Рассмотрим вопрос о назначении используемых в нем скобок. Здесь их две пары.



double (*pf)(double);

Вторая пара скобок – это оператор функция. Возникает вопрос: «Какую роль играет первая пара скобок?». С целью разобраться в этом вопросе на время удалим эту пару скобок. Тогда рассматриваемое выражение примет следующий вид.

double *pf(double);

Полученное выражение является объявлением функции, которая принимает один параметр типа double и возращает указатель типа double*. Для того чтобы убедиться в этом вспомним, что в синтаксис объявлений в языке Си заложен синтаксис выражений. Для определения назначения имени pf в рассматриваемом выражении следует рассмотреть подвыражение *pf(double). Здесь к имени pf применяются два оператора: * и () . Трактовка имени pf зависит от приоритетов этих операторов. Если обратиться к таблице приоритетов операторов, то можно убедиться в том, что оператор функция () имеет более высокий приоритет по сравнению с оператором разыменования *. Отсюда следует, что оператор () сильнее связывает имя pf по сравнению с оператором *. Итак, в новой редакции pf является именем функции, а не именем указателя.

Вернемся теперь к исходному объявленияю

double (*pf)(double);

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

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

Пусть имеются две функции, имеющие следующие прототипы.

double sqr(double x);
double cube(double x);

Объявим три указателя на функцию:

double (*pf1)(double) = &sqr;
double (*pf2)(double)= cube;

double (*pf3)(double)= 0;

Синтаксис, с которым объявлены первые две переменные (pf1 и pf2), несколько отличаются. Синтаксис, с которым объявлена первая переменная (pf1) не вызывает сомнений в корректности. Справа от знака присваивания находится выражение, значением которого является адрес функции. Могут возникнуть сомнения в корректности второго объявления, где объявлена переменная pf2. В этом объявлении в качестве инициализатора используется имя функции, а не ее адрес. Однако этот код нормально компилируется. Дело в том, что языке Си всегда в том коде, где ожидается указатель на функцию, а встречается имя функции, выполняется автоматическое преобразование имени функции в указатель.

Замечание . Операция инициализации выполняется и при использовании казателя на функцию в качестве формального параметра функции.



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: