Конструктор по умолчанию. Конструкторы и деструкторы C программирование конструктор объектов типы

Конструктор – это схожая c методом структура, назначение которой состоит в создании экземпляра класса. Характеристики конструктора:
  • Имя конструктора должно совпадать с именем класса (по договоренности, первая буква - заглавная, обычно имя существительное);
  • Конструктор имеется в любом классе. Даже если вы его не написали, компилятор Java сам создаст конструктор по умолчанию (default constructor), который будет пустым и не делает ничего, кроме вызова конструктора суперкласса.
  • Конструктор похож на метод, но не является методом, он даже не считается членом класса. Поэтому его нельзя наследовать или переопределить в подклассе;
  • Конструкторы не наследуются;
  • Конструкторов может быть несколько в классе. В этом случае конструкторы называют перегруженными;
  • Если в классе не описан конструктор, компилятор автоматически добавляет в код конструктор без параметров;
  • Конструктор не имеет возвращаемого типа, им не может быть даже тип void , если возвращается тип void , то это уже не конструктор а метод, несмотря на совпадение с именем класса.
  • В конструкторе допускается оператор return , но только пустой, без всякого возвращаемого значения;
  • В конструкторе допускается применение модификаторов доступа, можно задать один из модификаторов: public , protected , private или без модификатора.
  • Конструктор не может иметь модификаторов abstract , final , native , static или synchronized ;
  • Ключевое слово this cсылается на другой конструктор в этом же классе. Если используется, то обращение должно к нему быть первой строкой конструктора;
  • Ключевое слово super вызывает конструктор родительского класса. Если используется, должно обращение к нему быть первой строкой конструктора;
  • Если конструктор не делает вызов конструктора super класса-предка (с аргументами или без аргументов), компилятор автоматически добавляет код вызова конструктора класса-предка без аргументов;

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

Конструктор имеется в любом классе. Даже если вы его не написали, компилятор Java сам создаст конструктор по умолчанию (default constructor). Этот конструктор пустой и не делает ничего, кроме вызова конструктора суперкласса. Т.е. если написать: public class Example { } то это эквивалентно написанию: public class Example { Example () { super ; } } В данном случае явно класса предка не указано, а по умолчанию все классы Java наследуют класс Object поэтому вызывается конструктор класса Object . Если в классе определен конструктор с параметрами, а перегруженного конструктора без параметров нет, то вызов конструктора без параметров является ошибкой. Тем не менее, в Java, начиная с версии 1.5, можно использовать конструкторы с аргументами переменной длины. И если есть конструктор, имеющий аргумент переменной длины, то вызов конструктора по умолчанию ошибкой не будет. Не будет потому, что аргумент переменной длины может быть пустым. Например, следующий пример не будет компилироваться, однако если раскомментарить конструктор с аргументом переменной длины, то компиляция и запуск пройдут успешно и в результате работы строки кода DefaultDemo dd = new DefaultDemo() ; вызовется конструктор DefaultDemo(int ... v) . Естественно, что в данном случае необходимо пользоваться JSDK 1.5. Файл DefaultDemo.java class DefaultDemo { DefaultDemo (String s) { System. out. print ("DefaultDemo(String)" ) ; } /* DefaultDemo(int ... v) { System.out.println("DefaultDemo(int ...)"); } */ public static void main (String args ) { DefaultDemo dd = new DefaultDemo () ; } } Результат вывода программы при раскомментаренном конструкторе: DefaultDemo (int . . . ) Однако, в распространенном случае, когда в классе вообще не определено ни одного конструктора, вызов конструктора по умолчанию (без параметров) будет обязательным явлением, поскольку подстановка конструктора по умолчанию происходит автоматически.

При создании объекта последовательно выполняются следующие действия:
  • Ищется класс объекта среди уже используемых в программе классов. Если его нет, то он ищется во всех доступных программе каталогах и библиотеках. После обнаружения класса в каталоге или библиотеке выполняется создание, и инициализация статических полей класса. Т.е. для каждого класса статические поля инициализируются только один раз.
  • Выделяется память под объект.
  • Выполняется инициализация полей класса.
  • Отрабатывает конструктор класса.
  • Формируется ссылка на созданный и инициализированный объект. Эта ссылка и является значением выражения, создающего объект. Объект может быть создан и с помощью вызова метода newInstance() класса java.lang.Class . В этом случае используется конструктор без списка параметров.

Перегрузка конструкторов

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

Параметризированные конструкторы

Сигнатура конструктора – это количество и типы параметров, а также последовательность их типов в списке параметров конструктора. Тип возвращаемого результата не учитывается. Конструктор не возвращает никаких параметров. Это положение объясняет в некотором смысле, как Java различает перегруженные конструкторы или методы. Java различает перегруженные методы не по возвращаемому типу, а по числу, типам и последовательности типов входных параметров. Конструктор не может возвращать даже тип void , иначе он превратится в обычный метод, даже не смотря на сходство с именем класса. Следующий пример демонстрирует это. Файл VoidDemo.java class VoidDemo { /** * Это конструктор */ VoidDemo () { System. out. println ("Constructor" ) ; } void VoidDemo () { System. out. println ("Method" ) ; } public static void main (String s ) { VoidDemo m = new VoidDemo () ; } } В результате программа выведет: Constructor Это лишний раз доказывает, что конструктором является метод без возвращаемых параметров. Тем не менее, для конструктора можно задать один из трех модификаторов public , private или protected . И пример теперь будет выглядеть следующим образом: Файл VoidDemo2.java class VoidDemo2 { /** * Это конструктор */ public VoidDemo2 () { System. out. println ("Constructor" ) ; } /** * А это уже обычный метод, даже не смотря на сходство с * именем класса, поскольку имеется возвращаемый тип void */ private void VoidDemo2 () { System. out. println ("Method" ) ; } public static void main (String s ) { VoidDemo2 m = new VoidDemo2 () ; } } В конструкторе разрешается записывать оператор return , но только пустой, без всякого возвращаемого значения. Файл ReturnDemo.java class ReturnDemo { /** * В конструкторе допускается использование оператора * return без параметров. */ public ReturnDemo () { System. out. println ("Constructor" ) ; return ; } public static void main (String s ) { ReturnDemo r = new ReturnDemo () ; } }

Конструкторы, параметризированные аргументами переменной длины

В Java SDK 1.5 появился долгожданный инструмент – аргументы переменной длины для конструкторов и методов(variable-length arguments). До этого переменное количество документов обрабатывалось двумя неудобными способами. Первый из них был рассчитан на то, что максимальное число аргументов ограничено небольшим количеством и заранее известно. В таком случае можно было создавать перегружаемые версии метода, по одной на каждый вариант списка передаваемых в метод аргументов. Второй способ рассчитан на неизвестное заранее и большое количество аргументов. В этом случае аргументы помещались в массив, и этот массив передавался методу. Аргументы переменной длины чаще всего задействованы в последующих манипуляциях с инициализациями переменных. Отсутствие некоторых из ожидаемых аргументов конструктора или метода удобно заменять значениями по умолчанию. Аргумент переменной длины есть массив, и обрабатывается как массив. Например, конструктор для класса Checking с переменным числом аргументов будет выглядеть так: class Checking { public Checking (int . . . n) { } } Символьная комбинация... сообщает компилятору о том, что будет использоваться переменное число аргументов, и что эти аргументы будут храниться в массиве, значение ссылки на который содержится в переменной n. Конструктор может вызываться с разным числом аргументов, включая их полное отсутствие. Аргументы автоматически помещаются в массив и передаются через n. В случае отсутствия аргументов длина массива равна 0. В список параметров наряду с аргументами переменной длины могут быть включены и обязательные параметры. В этом случае параметр, содержащий переменное число аргументов должен обязательно быть последним в списке параметров. Например: class Checking { public Checking (String s, int . . . n) { } } Вполне очевидное ограничение касается количества параметров с переменной длиной. В списке параметров должен быть только один параметр переменной длины. При наличии двух параметров переменной длины компилятору невозможно определить, где заканчивается один параметр и начинается другой. Например: class Checking { public Checking (String s, int . . . n, double . . . d) //ОШИБКА! { } } Файл Checking.java Например, есть аппаратура, способная распознавать номера автомобилей и запоминать номера квадратов местности, где побывал каждый из автомобилей за день. Необходимо из общей массы зафиксированных автомобилей отобрать те, которые в течение дня побывали в двух заданных квадратах, скажем 22 и 15, согласно карте местности. Вполне естественно, что автомобиль может в течение дня побывать во многих квадратах, а может только в одном. Очевидно, что количество посещенных квадратов ограничено физической скоростью автомобиля. Составим небольшую программу, где конструктор класса будет принимать в качестве аргументов номер автомобиля как обязательный параметр и номера посещенных квадратов местности, число которых может быть переменным. Конструктор будет проверять, не появился ли автомобиль в двух квадратах, если появился, то вывести его номер на экран.

Передача параметров в конструктор

В языках программирования существует в основном два вида параметров:
  • основные типы (примитивы);
  • ссылки на объекты.
Термин вызов по значению (call by value) означает, что конструктор получает значение, переданное ему вызывающим модулем. В противоположность этому, вызов по ссылке (call by reference) означает, что конструктор получает от вызывающего модуля адрес переменной. В языке Java используется только вызов по значению. По значению параметра и по значению ссылки параметра. Java не использует вызов по ссылке для объектов (хотя многие программисты и авторы некоторых книг это утверждают). Параметры при передаче объектов в Java осуществляются не по ссылке , а по значению ссылки на объекты ! В любом случае конструктор получает копии значений всех параметров. Конструктор не может делать со своими входными параметрами:
  • конструктор не может менять значения входных параметров основных (примитивных) типов;
  • конструктор не может изменять ссылки входных параметров;
  • конструктор не может переназначать ссылки входных параметров на новые объекты.
Конструктор может делать со своими входными параметрами:
  • изменять состояние объекта, передаваемого в качестве входного параметра.
Следующий пример доказывает, что в Java входные параметры для конструктора передаются по значению ссылки на объект. Так же в этом примере отражено то, что конструктор не может изменять ссылки входных параметров, а фактически изменяет ссылки копий входных параметров. Файл Empoyee.java class Employee { Employee (String x, String y) { String temp = x; x = y; y = temp; } public static void main (String args ) { String name1 = new String ("Alice" ) ; String name2 = new String ("Mary" ) ; Employee a = new Employee (name1, name2) ; System. out. println ("name1=" + name1) ; System. out. println ("name2=" + name2) ; } } Результат вывода программы: name1= Alice name2= Mary Если бы в языке Java для передачи объектов в качестве параметров использовался вызов по ссылке, то конструктор поменял бы в этом примере местами name1 и name2 . На самом деле конструктор не поменяет местами объектные ссылки, хранящиеся в переменных name1 и name2 . Это говорит о том, что параметры конструктора инициализируются копиями этих ссылок. Затем конструктор меняет местами уже копии. По завершении работы конструктора переменные x и y уничтожаются, а исходные переменные name1 и name2 продолжают ссылаться на прежние объекты.

Изменение параметров, передаваемых конструктору.

Конструктор не может модифицировать передаваемые параметры основных типов. Однако, конструктор может модифицировать состояние объекта, передаваемого как параметр. Например, рассмотрим следующую программу: Файл Salary1.java class Salary1 { Salary1 (int x) { x = x * 3 ; System. out. println ("x=" + x) ; } public static void main (String args ) { int value = 1000 ; Salary1 s1 = new Salary1 (value) ; System. out. println ("value=" + value) ; } } Результат вывода программы: x= 3000 value= 1000 Очевидно, что такой способ не изменит параметр основного типа. Поэтому после вызова конструктора значение переменной value остается равным 1000 . По сути, происходит три действия:
  1. Переменная x инициализируется копией значения параметра value (т.е. числом 1000).
  2. Значение переменной x утраивается – теперь оно равно 3000 . Однако значение переменной value остается равным 1000 .
  3. Конструктор завершает свою работу, и переменная x больше не используется.
В следующем примере зарплата сотрудника успешно утраивается, так как в качестве параметра методу передается значение ссылки объекта. Файл Salary2.java class Salary2 { int value = 1000 ; Salary2 () { } Salary2 (Salary2 x) { x. value = x. value * 3 ; } public static void main (String args ) { Salary2 s1 = new Salary2 () ; Salary2 s2 = new Salary2 (s1) ; System. out. println ("s1.value=" + s1. value) ; System. out. println ("s2.value=" + s2. value) ; } } Результат вывода программы: s1. value= 3000 s2. value= 1000 В качестве параметра используется значение ссылки на объект. При выполнении строки Salary2 s2 = new Salary2(s1) ; конструктору Salary2(Salary x) передастся значение ссылки на объект переменной s1 , и конструктор фактически утроит зарплату для s1.value , поскольку даже копия (Salary x) , создаваемая внутри конструктора указывает на объект переменной s1 .

Конструкторы, параметризированные примитивами.

В случае, если в параметрах перегруженного конструктора используется примитив, который может быть сужен (например int <- double), то вызов метода со суженным значением возможен, несмотря на то, что метода, перегруженного с таким параметром нет. Например: Файл Primitive.java class Primitive { Primitive (double d) { d = d + 10 ; System. out. println ("d=" + d) ; } public static void main (String args ) { int i = 20 ; Primitive s1 = new Primitive (i) ; } } Результат вывода программы: d= 30.0 Несмотря на то, что в классе Primitive отсутствует конструктор, у которого есть параметр типа int , отработает конструктор с входным параметром double . Перед вызовом конструктора переменная i будет расширена от типа int до типа double . Обратный вариант, когда переменная i была бы типа double , а конструктор был бы только с параметром int , в данной ситуации привел бы к ошибке компиляции.

Вызов конструктора и оператор new

Конструктор всегда вызывается оператором new . При вызове конструктора оператором new , конструктор всегда формирует ссылку на новый объект. Заставить конструктор сформировать вместо ссылки на новый объект ссылку на уже существующий объект нельзя, кроме подстановки десериализируемого объекта. А с оператором new сформировать вместо ссылки на новый объект ссылку на уже существующий объект нельзя. Например: Файл Salary3.java class Salary3 { int value = 1000 ; Salary3 () { } Salary3 (Salary3 x) { x. value = x. value * 3 ; } public static void main (String args ) { Salary3 s1 = new Salary3 () ; System. out. println ("First object creation: " + s1. value) ; Salary3 s2 = new Salary3 (s1) ; System. out. println ("Second object creation: " + s2. value) ; System. out. println (+ s1. value) ; Salary3 s3 = new Salary3 (s1) ; System. out. println ("Third object creation: " + s3. value) ; System. out. println ("What"s happend with first object?:" + s1. value) ; } } Результат вывода программы: First object creation: 1000 Second object creation: 1000 What"s happend with first object? : 3000 Third object creation: 1000 What"s happend with first object? : 9000 Сначала с помощью строчки Salary3 s1 = new Salary3() ; создается новый объект. Далее, если бы с помощью строки Salary3 s2 = new Salary3(s1) ; или строки Salary3 s3 = new Salary3(s1) ; можно было бы создать ссылку на уже существующий объект, то s1.value s2.value и s3.value хранили бы одинаковое значение 1000 . На самом деле в строке Salary3 s2 = new Salary3(s1) ; создастся новый объект для переменной s2 и изменится состояние объекта для переменной s1 , через передачу своего значения ссылки на объект, в параметре конструктора. В этом можно убедиться по результатам вывода. А при выполнении строки Salary3 s3 = new Salary3(s1) ; создастся НОВЫЙ объект для переменной s3 и снова изменится состояние объекта для переменной s1 .

Конструкторы и блоки инициализации, последовательность действий при вызове конструктора

В разделе Создание объекта и конструкторы перечислены действия общего характера, которые производятся при создании объекта. Среди них сопрягаются процессы инициализация полей класса и отработка конструктора класса, которые в свою очередь тоже имеют внутренний порядок:
  1. Все поля данных инициализируются своими значениями, предусмотренными по умолчанию (0, false или null).
  2. Инициализаторы всех полей и блоки инициализации выполняются в порядке их перечисления в объявлении класса.
  3. Если в первой строке конструктора вызывается другой конструктор, то выполняется вызванный конструктор.
  4. Выполняется тело конструктора.
Конструктор имеет отношение к инициализации, поскольку в Java существует три способа инициализации поля в классе:
  • присвоить значение в объявлении;
  • присвоить значения в блоке инициализации;
  • задать его значение в конструкторе.
Естественно, нужно организовать код инициализации так, чтобы в нем было легко разобраться. В качестве примера приведен следующий класс: class Initialization { int i; short z = 10 ; static int x; static float y; static { x = 2000 ; y = 3.141 ; } Initialization () { System. out. println ("i=" + i) ; System. out. println ("z=" + z) ; z = 20 ; System. out. println ("z=" + z) ; } } В приведенном примере, переменные инициализируются в следующем порядке: сначала инициализируются статические переменные x и y значениями по умолчанию. Далее выполняется статический блок инициализации. Затем производится инициализация переменной i значением по умолчанию и инициализируется переменная z . Далее в работу вступает конструктор. Вызов конструкторов класса не должен зависеть от порядка объявления полей. Это может привести к ошибкам.

Конструкторы и наследование

Конструкторы не наследуются. Например: public class Example { Example () { } public void sayHi () { system. out. println ("Hi" ) ; } } public class SubClass extends Example { } Класс SubClass автоматически наследует метод sayHi() определенный в родительском классе. В тоже время, конструктор Example() родительского класса не наследуется его потомком SubClass .

Ключевое слово this в конструкторах

Конструкторы используют this чтобы сослаться на другой конструктор в этом же классе, но с другим списком параметров. Если конструктор использует ключевое слово this , то оно должно быть в первой строке, игнорирование этого правила приведет к ошибке компилятора. Например: Файл ThisDemo.java public class ThisDemo { String name; ThisDemo (String s) { name = s; System. out. println (name) ; } ThisDemo () { this ("John" ) ; } public static void main (String args ) { ThisDemo td1 = new ThisDemo ("Mary" ) ; ThisDemo td2 = new ThisDemo () ; } } Результат вывода программы: Mary John В данном примере имеется два конструктора. Первый получает строку-аргумент. Второй не получает никаких аргументов, он просто вызывает первый конструктор используя имя "John" по-умолчанию. Таким образом, можно с помощью конструкторов инициализировать значения полей явно и по умолчанию, что часто необходимо в программах.

Ключевое слово super в конструкторах

Конструкторы используют super , чтобы вызвать конструктор суперкласса. Если конструктор использует super , то этот вызов должен быть в первой строке, иначе компилятор выдаст ошибку. Ниже приведен пример: Файл SuperClassDemo.java public class SuperClassDemo { SuperClassDemo () { } } class Child extends SuperClassDemo { Child () { super () ; } } В этом простом примере конструктор Child() содержит вызов super() , который создает экземпляр класса SuperClassDemo , в дополнение к классу Child . Так как super должен быть первым оператором, выполняемым в конструкторе подкласса, этот порядок всегда одинаков и не зависит от того, используется ли super() . Если он не используется, то сначала будет выполнен конструктор по умолчанию (без параметров) каждого суперкласса, начиная с базового класса. Следующая программа демонстрирует, когда выполняются конструкторы. Файл Call.java //Создать суперкласс A class A { A () { System. out. println ("Inside A constructor." ) ; } } //Создать подкласс B, расширяющий класс A class B extends A { B () { System. out. println ("Inside B constructor." ) ; } } //Создать класс (C), расширяющий класс В class C extends B { C () { System. out. println ("Inside C constructor." ) ; } } class Call { public static void main (String args ) { C c = new C () ; } } Вывод этой программы: Inside A constructor. Inside B constructor. Inside C constructor. Конструкторы вызываются в порядке подчиненности классов. В этом есть определенный смысл. Поскольку суперкласс не имеет никакого знания о каком-либо подклассе, то любая инициализация, которую ему нужно выполнить, является отдельной. По возможности она должна предшествовать любой инициализации, выполняемой подклассом. Поэтому-то она и должна выполняться первой.

Настраиваемые конструкторы

Механизм идентификации типа во время выполнения является одним из мощных базовых принципов языка Java, который реализует полиморфизм. Однако такой механизм не страхует разработчика от несовместимого приведения типов в ряде случаев. Самый частый случай – манипулирование группой объектов, различные типы которых заранее неизвестны и определяются во время выполнения. Поскольку ошибки, связанные с несовместимостью типов могут проявиться только на этапе выполнения, то это затрудняет их поиск и ликвидацию. Введение настраиваемых типов в Java 2 5.0 частично отодвигает возникновение подобных ошибок с этапа выполнения на этап компиляции и обеспечивает недостающую типовую безопасность. Отпадает необходимость в явном приведении типов при переходе от типа Object к конкретному типу. Следует иметь ввиду, что средства настройки типов работают только с объектами и не распространяются на примитивные типы данных, которые лежат вне дерева наследования классов. Благодаря настраиваемым типам все приведения выполняются автоматически и скрыто. Это позволяет обезопасить от несоответствия типов и гораздо чаще повторно использовать код. Настраиваемые типы можно использовать в конструкторах. Конструкторы могут быть настраиваемыми, даже если их класс не является настраиваемым типом. Например: class GenConstructor { private double val; < T extends Number > GenConstructor (T arg) { val = arg. doubleValue () ; } void printValue () { System. out. println ("val: " + val) ; } } class GenConstructorDemo { public static void main (String args ) { GenConstructor gc1 = new GenConstructor (100 ) ; GenConstructor gc2 = new GenConstructor (123.5F ) ; gc1. printValue () ; gc2. printValue () ; } } Поскольку конструктор GenConstructor задает параметр настраиваемого типа, который должен быть производным классом от класса Number , его можно вызвать с любы

Функции-члены класса

#include

// содержит функции ввода-вывода

#include

// содержит функцию CharToOem

#include

// содержит математические функции

class Dot

// класс точки

// закрытые члены класса

char name ;

// имя точки

double x , y ;

// координаты точки

// открытые члены класса public :

// конструктор с параметрами

Dot (char Name , double X , double Y) { name = Name ; x = X ; y = Y ; }

void Print () ;

Здесь значение, переданное в конструктор при объявлении объекта A , используется для инициализации закрытых членов name , x и y этого объекта.

Фактически синтаксис передачи аргумента конструктору с параметрами является сокращенной формой записи следующего выражения:

Dot A = Dot ("A" , 3 , 4) ;

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

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

Правила для конструкторов

Приведем правила, которые существуют для конструкторов:

конструктор класса вызывается всякий раз, когда создается объект его класса;

конструктор обычно инициализирует данные-члены класса и резервирует память для динамических членов;

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

класс может иметь несколько перегруженных конструкторов;

конструктор не может быть объявлен с модификатором const , volatile , static или virtual ;

если в классе не определен конструктор, компилятор генерирует конструктор по умолчанию , не имеющий параметров.

Правила для деструкторов

Для деструкторов существуют следующие правила: деструктор вызывается при удалении объекта;

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

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

деструктор не может быть объявлен с модификатором const , volatile или static ;

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

Подчеркнем еще раз: конструкторы и деструкторы в C++ вызываются автоматически, что гаран-

тирует правильное создание и удаление объектов класса.

Список инициализации элементов

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

ConstrName (parl , par2) : mem1 (parl) , mem2 (par2) , … { …}

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

class Dot

char name ;

// имя точки

double x , y ;

// координаты точки

public:

// конструктор со списком инициализации

Dot (char Name) : name (Name) , x (0) , y (0) { }

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

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

C++ определяет два специальных вида конструкторов: конструктор по умолчанию, о котором мы упоминали выше, и конструктор копирования. Конструктор по умолчанию не имеет параметров (или все его параметры должны иметь значения по умолчанию) и вызывается при создании объекта, которому не заданы аргументы. Следует избегать двусмысленности при вызове конструкторов. В приведенном ниже примере два конструктора по умолчанию являются двусмысленными:

class Т

public:

// конструктор по умолчанию

Т (int i = 0) ;

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

// может быть использован как конструктор по умолчанию

void main ()

// Использует конструктор T::T (int)

// Не верно; неоднозначность вызова Т::Т () или Т::Т (int = 0)

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

Конструкторы копирования

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

рования по умолчанию. В C++ различают поверхностное и глубинное копирование данных.

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

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

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

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

class Dot

// класс точки

// закрытые члены класса

char name ;

// имя точки

double x , y ;

// координаты точки

public:

// открытые члены класса

// конструкторы с параметрами

Dot (char Name , double X , double Y)

{ name = Name ; x = X ; y = Y ; }

Dot (char Name) : name(Name) , x (0) , y (0)

Dot (char Name , const Dot& A)

{ name = Name ; x = A.x ; y = A.y ; }

// конструктор копирования

Dot (const Dot& A)

{ name = (char ) 226 ; x = A.x ; y = A.y ; }

void Print () ;

// выводит на экран имя и координаты текущей точки

void main ()

Dot A ("A", 3 , 4) ;

// вызов конструктора Dot (char Name , double X , double Y)

// вызов конструктора Dot (char Name)

// выводит на экран:

Координаты точки т :

// вызов конструктора копирования Dot (const Dot& A)

// выводит на экран:

Координаты точки т :

Dot E ("E", A) ;

// вызов конструктора Dot (char Name , const Dot& A)

// выводит на экран:

Координаты точки E:

Конструктор копирования по умолчанию выполняет поверхностное копирование, которое в нашей задаче копирует не только координаты, но и имена точки. Хотя наш объект не содержит динамических чле-

class A { public: A():a(0),b(0) {} explicit A(int x): a(x), b(0) {} A(int x, int y): a(x), b(y) {} private: int a,b; };

Class A { public: explicit A(int x=0, int y=0): a(x), b(y) {} private: int a,b; };

Есть ли различия? Что лучше использовать?

2 ответа

Vlad from Moscow

Эти два объявления классов не эквивалентны.

Во втором объявлении класса конструктор объявлен со спецификатором функции explicit , а это ограничивает применение этого конструктора в различных ситуациях.

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

То есть первое объявление предоставляет больше возможностей по использованию класса.

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

#include struct A { explicit A(int x = 0, int y = 0) : x(x), y(y) {} int x; int y; }; struct B { B() : x(0), y(0) {} explicit B(int x): x(x), y(0) {} B(int x, int y): x(x), y(y) {} int x; int y; }; void f(const A &a) { std::cout << "a.x = " << a.x << ", a.y = " << a.y << std::endl; } void g(const B &b) { std::cout << "b.x = " << b.x << ", b.y = " << b.y << std::endl; } int main() { // f({}); // f({ 1, 2 }); g({}); g({ 1, 2 }); }

Ее вывод на консоль:

B.x = 0, b.y = 0 b.x = 1, b.y = 2

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

Другое важное отличии состоит в том, что один класс имеет всего лишь один конструктор с заданной сигнатурой, а другой класс имеет три конструктора с различными сигнатурами.

Рассмотрите еще один демонстрационный пример

Struct A { explicit A(int x = 0, int y = 0) : x(x), y(y) {} int x; int y; }; struct B { B() : x(0), y(0) {} explicit B(int x): x(x), y(0) {} B(int x, int y): x(x), y(y) {} int x; int y; }; struct C { //friend A::A(); friend B::B(); }; int main() { }

Здесь в классе C вы можете объявить конструктор по умолчанию класса B в качестве друга класса С. Однако вы не можете сделать то же самое с конструктором по умолчанию класса A , чтобы объявить его другом класса C , так как конструктор по умолчанию в классе A имеет другую сигнатуру.

Вам уже придется писать

Struct C { friend A::A(int, int); };

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

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

Если рассматривать не конструкторы, а функции, то разница имеется еще более существенная.

Аргументы по умолчанию не влияют на тип функции. Поэтому, например, если вы объявили функцию как

Void f(int, int = 0);

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

F(value);

тем не менее ее тип void(int, int) . А это в свою очередь означает, что вы не можете, например, написать

Void h(void f(int)) { f(10); } void f(int x, int y = 0) { std::cout << "x = " << x << ", y = " << y << std::endl; } // h(f);

так как параметр функции h имеет тип void(int) ., а у функции, используемой в качестве аргумента, тип void(int, int)

Если же вы объявите две функции вместо одной

Void h(void f(int)) { f(10); } void f(int x) { std::cout << "x = " << x << std::endl; } void f(int x, int y) { std::cout << "x = " << x << ", y = " << y << std::endl; }

то данный вызов

будет корректным, так как имеется функция с одним параметром.

IxSci

Различия уже объяснил @Vlad from Moscow, я лишь предложу, из двух вариантов в вопросе, третий вариант:

Class A { public: A():A(0, 0) {} explicit A(int x): A(x, 0) {} A(int x, int y): a{x}, b{y} {} private: int a,b; };

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

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

Понятие «Конструктор» (constructor) неотделимо от понятия класса. Конструкторы — это специальные функции, вызываемые автоматически при инициализации объектов. Их имена совпадают с именами классов, которым они принадлежат, и они не имеют типа возврата. У одного класса может быть более одного конструктора, различающихся сигнатурами. Конструкторы полезны для инициализации полей класса. Интересно, что компилятор создает конструктор по умолчанию без параметров, он устанавливает поля в 0, false или null (для объектов).

Рассмотрим пример. Создадим класс «Треугольник» с тремя полями и одним методом, вычисляющим его периметр.

Using System; namespace Конструктор { class Triangle { int a; int b; int c; public int perimeter() { return a + b + c; } } class Program { static void Main(string args) { Triangle tr = new Triangle(); // создаем объект Console.WriteLine("периметр = " + tr.perimeter()); Console.ReadKey(); } } }

периметр = 0

АНАЛИЗ

В окне ошибок первый раз появляются 3 предупреждения (но не ошибки!):
Полю «Конструктор.Triangle.c» нигде не присваивается значение, поэтому оно всегда будет иметь значение по умолчанию 0 (тоже для полей a и b ).

Так как сработал конструктор без параметров Triangle(), то всем сторонам треугольника был присвоен 0. Добавим в определение класса строку: Triangle(); и запустим программу. Получим сообщение об ошибке:
Тип «Конструктор.Triangle» уже определяет член «Triangle» с такими же типами параметров.
Это подтверждает тот факт, что конструктор с нулем параметров уже был создан. Теперь добавим в описание класса конструктор с тремя параметрами:

Public Triangle(int a, int b, int c) { this.a = a; this.b = b; this.c = c; }

А в метод Main() первую строку заменим на:

Triangle tr = new Triangle(3,4,5);

Результат выполнения программы: периметр = 12

То есть, мы выполнили инициализацию полей (a, b, c ) объекта tr , на что указывает ключевое слово this в операторах конструктора. Иначе нам бы пришлось указывать разные имена: a = d; где d был бы тогда первым элементом в списке параметров. Оператор типа a = a оставит значение поля a=0, а компилятор выдаст предупреждение:
Проведено присвоение той же переменной; действительно выполнить такое назначение, а не иное?

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

Напомним, что так как по умолчанию поля класса являются закрытыми (private), то их нельзя изменить непосредственно в программе, например:
tr.c = 7;
В этом случае получим сообщение об ошибке:
«Конструктор.Triangle.c» недоступен из-за его уровня защиты.

Ошибка устранится, если к описанию поля с добавим модификатор public:
public int c;
Однако это не является хорошим тоном в ООП, поле перестает быть защищенным.

В отличие от структур классы относятся к ссылочным (reference) типам данных, их экземпляры (объекты) “живут” в куче (heap). Поэтому при создании объекта резервируется для полей место в куче, и в зависимости от конструктора полям задаются значения 0, false или null (для конструктора по умолчанию), либо соответствующие значения (для конструктора с параметрами).

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

1) Можно ли создать конструктор по умолчанию? — Да.
2) Если создается свой конструктор, будет ли компилятор генерировать конструктор по умолчанию? — Нет.
3) Если в своём конструкторе не будут инициализированы некоторые поля, будут ли они автоматически инициализированы компилятором? — Да.
4) Разрешается ли инициализировать переменные там, где их объявляют? — Да.

Пример конструктора при наследовании класса

Воспользуемся готовым классом Random из библиотеки System. Создадим дочерний класс RwName («Random with Name»), добавим в него поле s и два метода, а также модифицируем конструктор класса:

Using System; namespace Конструктор { class RwName: Random { public string s; public RwName(string s) { this.s = s; } public double NextD() { return this.NextDouble(); } public void Rezult() { Console.WriteLine(this.s + this.NextDouble()); } } class Program { static void Main(string args) { RwName r = new RwName("Вещественное число: "); Console.WriteLine(r.NextD()); r.Rezult(); Console.ReadKey(); } } }

Теперь объект класса получит наименование через конструктор, а два метода класса стали более специфичными. Даже если вы не указали в объявлении класса имя «родителя», он все равно будет наследовать конструктор класса Object.

Статический конструктор

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

Using System; namespace Конструктор { class StatClass { public static int q; public int p; // Статический конструктор static StatClass() { q = 33; } // Обычный конструктор public StatClass() { p = 44; } } class Program { static void Main(string args) { StatClass ob = new StatClass(); Console.WriteLine("Доступ к полю p экземпляра: " + ob.p); Console.WriteLine("Доступ к полю q: " + StatClass.q); Console.ReadLine(); } } }

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

ВЫВОД

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

Конструктор — функция, предназначенная для инициализации объектов класса.Рассмотрим класс date :

class date
{
int day, month, year;
public :
set(int , int , int );
};

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

class date {
int day, month, year;
public :
date(int , int , int ); // конструктор
};

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

date today = date(6,4,2014); // полная форма
date xmas(25,12,0); // сокращенная форма
// date my_burthday; // недопустимо, опущена инициализация

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

class date {
int month, day, year;
public :
date(int , int , int ); // день месяц год
date(char *); // дата в строковом представлении
date(); // дата по умолчанию: сегодня
};

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

date july4("Февраль 27, 2014" );
date guy(27, 2, 2014);
date now; // инициализируется по умолчанию

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

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

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

class date
{
int month, day, year;
public :
date(int , int , int );
date(char *);
date(); // конструктор по умолчанию
};

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

date date2 = date1;

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

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

Во всех этих случаях транслятор не вызывает конструктора для вновь создаваемого объекта:

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

Вместо этого в них копируется содержимое объекта-источника:

  • date1 в приведенном примере;
  • фактического параметра;
  • объекта-результата в операторе return .

Конструктор копии

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

class String
{
char *str;
int size;
public :
String(String&); // Конструктор копирования
};
String::String(String& right) { // Создает копии динамических
// переменных и ресурсов
str = new char ;
strcpy(str, right->str);
size = right->size;
}

Деструкторы

Определяемый пользователем класс имеет конструктор, который обеспечивает надлежащую инициализацию. Для многих типов также требуется обратное действие. Деструктор обеспечивает соответствующую очистку объектов указанного типа. Имя деструктора представляет собой имя класса с предшествующим ему знаком «тильда» ~ . Так, для класса X деструктор будет иметь имя ~X() . Многие классы используют динамическую память, которая выделяется конструктором, а освобождается деструктором.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class date
{
int day, year;
char *month;
public :
date(int d, char * m, int y)
{
day = d;
month = new char ;
strcpy_s(month, strlen(m)+1,m);
year = y;
}
~date() { delete month; } // деструктор
};


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

Конструктор нового класса имеет пустое тело и список вызываемых конструкторов класса vect , перечисленных после двоеточия (:) через запятую (,). Они выполняются с целым аргументом i , создавая 3 объекта класса vect: a, b, c .

Конструкторы членов класса всегда выполняются до конструктора класса, в котором эти члены описаны. Порядок выполнения конструкторов для членов класса определяется порядком объявления членов класса. Если конструктору члена класса требуются аргументы, этот член с нужными аргументами указывается в списке инициализации. Деструкторы вызываются в обратном порядке.
{
system("chcp 1251" );
system("cls" );
multi_v f(3);
for (int i = 0; i <= f.getSize(); i++)
{
f.a.element(i) = 10 + i;
f.b.element(i) = 20 + 5 * i;
f.c.element(i) = 120 + 5 * i;
}
for (int i = 0; i <= f.getSize(); i++)
{
cout << f.a.element(i) << "лет \t" ;
cout << f.b.element(i) << "кг \t" ;
cout << f.c.element(i) << "см" << endl;
}
cin.get();
return 0;
}


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