Анимация
JavaScript
|
Главная Библионтека конструкторы с аргументами, но не объявите конструктора без аргументов, то компилятор не позволит конструировать объекты этого класса, даже в качестве базового для другого класса, с использованием конструктора без аргументов. class Foo { public: Foo(char*); Foo f; Нельзя - нет конструктора без аргументов! class Bar : public Foo { public: Bar(); Bar::Bar() Ошибка! Нет конструктора Foo без аргументов Списки инициализации членов Чтобы избавиться от этой проблемы, в C++ находится очередное применение символу : - для создания списков инициализации членов. Так называется список спецификаций конструкторов, разделенных занятыми и расположенных между сигнатурой конструктора и его телом. class Foo { public: Foo(char*); class Bar : public Foo { public: Bar(char*); class BarBar { private: Foo f; int x; public: BarBar(); Bar::Bar(char* s) : Foo(s) {...} BarBar::BarBar : f("He11o"), x(17) {...} В конструкторе Bar список инициализации членов используется для инициализации базового класса Foo. Компилятор выбирает используемый конструктор на основании сигнатуры, определяемой по фактическим аргументам. При отсутствии списка инициализации членов сконструировать Bar было бы невозможно, поскольку компилятор не мог бы определить, какое значение должно передаваться конструктору базового класса Foo. В конструкторе BarBar список инициализации членов использовался для инициализации (то есть вызова конструкторов) переменных f и х. В следующем варианте конструктор работает не столь эффективно (если только компилятор не отличается сверхъестественным интеллектом): BarBar::BarBar() : f("He11o") x = 17; Во втором варианте переменная х сначала инициализируется значением 0 (стандартное требование C++) с использованием по умолчанию конструктора int без аргументов, а затем в теле конструктора ей присваивается значение 17. В первом варианте имеется всего одна инициализация и потому экономится один-два машинных такта. В данном примере это несущественно, поскольку переменная х - целая, но если бы она относилась к более сложному классу с конструктором без аргументов и перегруженным оператором присваивания, то разница была бы вполне ощутима. Списки инициализации членов нужны там, где у базового класса или переменной нет конструктора без аргументов (точнее, есть один и более конструктор с аргументами, но нет ни одного определенного пользователем конструктора без аргументов). Списки инициализации членов не обязательны в тех ситуациях, когда все базовые классы и переменные класса либо не имеют конструкторов, либо имеют пользовательский конструктор без аргументов. Порядок вызова конструкторов Если класс не содержит собственных конструкторов, он инициализируется так, словно компилятор создал конструктор без аргументов за вас. Этот конструктор вызывает конструкторы без аргументов базовых классов и переменных класса. Четко определенный порядок вызова конструкторов не зависит от того, используются конструкторы стандартные или перегруженные, с аргументами или без: 1 . Сначала вызываются конструкторы базовых классов в порядке их перечисления в списке наследования (еще один список, в котором после символа : перечисляются базовые классы, разделенные запятыми). 2. Затем вызываются конструкторы переменных класса в порядке их объявления в объявлении класса. 3. После того как будут сконструированы все базовые классы и переменные, выполняется тело вашего конструктора. Описанный порядок применяется рекурсивно, то есть первым конструируется первый базовый класс первого базового класса... и т. д. Он не зависит от порядка, указанного в списке инициализации членов. Если бы дело обстояло иначе, для разных перегруженных конструкторов мог бы использоваться разный порядок конструирования. Тогда компилятору было бы трудно гарантировать, что деструкторы будут вызываться в порядке, обратном порядку вызова конструкторов. Конструкторы копий Конструктор копий (copy constructor) определяется специальной сигнатурой: class Foo { public: Foo(const Foo&); Foo::Foo(const Foo& f)... Конструктор копий предназначен для создания копий объектов. Эта задача может возникнуть в самых разных обстоятельствах. void Fn(Foo f) {...} void Gn(Foo& f) {...} Foo f; Foo f1(f); Foo f2 = f; Конструирование, а не присваивание! Fn(f); Вызывает конструктор копий для передачи по назначению const Foo f3; Gn(f3); Конструктор копий используется для создания неконстантной копии Давайте внимательно рассмотрим этот фрагмент. Строка Foo f1(f); создает новый экземпляр класса Foo, передавая другой экземпляр класса Foo в качестве аргумента. Это всегда можно сделать, если класс Foo не содержит чисто виртуальных функций. Не важно, объявили ли вы свой собственный конструктор копий; если нет, компилятор построит его за вас. Не важно, есть ли в Foo другие пользовательские конструкторы; в отличие от конструкторов без аргументов, конструктор копий доступен всегда. Строка Foo f2 = f выглядит как присваивание из-за присутствия оператора =, но на самом деле это альтернативный вариант вызова конструктора копий. Чтобы понять, чем присваивание отличается от инициализации, спросите себя: «Был ли объект сконструирован заранее или же его создание является частью команды?» Если объект уже существует, вы имеете дело с присваиванием. Если он создается на месте, как в пашем примере, используется конструктор копий. При вызове функции Fn() происходит передача по значению копии Foo. Конструктор копий используется для создания временной копии, существующей лишь во время выполнения Fn(). После этого вызывается деструктор копии, который уничтожает ее. Вызов функции Gn(), вероятно, ошибочен, и хороший компилятор прочитает вам суровую нотацию о стиле программирования на C++ - что-нибудь вроде: «Создается временная неконстантная копия - поучись программировать, тупица!» По крайней мере, со мной компиляторы обычно поступают именно так. Проблема заключается в том, что аргумент передается по ссылке, однако фактический аргумент является константным, а формальный - нет. Все изменения аргумента внутри Gn() вносятся в копию, а не в оригинал. В создаваемом компилятором конструкторе копий по умолчанию используется строго определенная последовательность вызова конструкторов копий базовых классов и переменных класса. 1 . Конструкторы копий базовых классов вызываются в том порядке, в котором они объявлены в списке наследования. 2. Конструкторы копий переменных вызываются в том порядке, в котором они объявлены в объявлении класса. Описанный порядок применяется рекурсивно, то есть первым копируется первый базовый класс первого базового класса... и т. д. Звучит знакомо, не правда ли? Тот же порядок, что и для любого другого конструктора. С конструкторами копий, в отличие от всех остальных, компилятор ведет себя гордо и ревниво. Если вы перегрузите конструктор копий для некоторого класса, компилятор, фигурально выражаясь, умывает руки и отправляется домой. При отсутствии явного вызова конструкторов копий базовых классов и переменных класса в списке инициализации членов вашего собственного конструктора копий компилятор будет использовать конструктор без аргументов для инициализации базовых классов и переменных. class Foo {...}; class Bar : public Foo { private: Foo f; public: Bar(const Bar&); Вероятно, ошибка Bar::Bar(const Bar& b) Стоп! Нет списка инициализации членов Будут использованы конструкторы без аргументов базового класса и переменной Вероятно, ошибки нет Bar::Bar(const Bar& b) : Foo(b), f(b.f) {...} 0 1 2 3 4 5 6 [ 7 ] 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |