Конструкторы представляют специальную функцию, которая имеет то же имя, что и класс, которая не возвращает никакого значения и которая позволяют инициалилизировать объект класса во время го создания и таким образом гарантировать, что поля класса будут иметь определенные значения. При каждом создании нового объекта класса вызывается конструктор класса.
В прошлой теме был разработан следующий класс:
#include <iostream> class Person { public: std::string name; unsigned age; void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } }; int main() { Person person; // вызов конструктора person.name = "Tom"; person.age = 22; person.print(); }
Здесь при создании объекта класса Person, который называется person
Person person;
вызывается конструктор по умолчанию. Если мы не определяем в классе явным образом конструктор, как в случае выше, то компилятор автоматически компилирует конструктор по умолчанию. Подобный конструктор не принимает никаких параметров и по сути ничего не делает.
Теперь определим свой конструктор. Например, в примере выше мы устанавливаем значения для полей класса Person. Но, допустим, мы хотим, чтобы при создании объекта эти поля уже имели некоторые значения по умолчанию. Для этой цели определим конструктор:
#include <iostream> class Person { public: std::string name; unsigned age; void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } Person(std::string p_name, unsigned p_age) { name = p_name; age = p_age; std::cout << "Person has been created" << std::endl; } }; int main() { Person tom("Tom", 38); // создаем объект - вызываем конструктор tom.print(); }
Теперь в классе Person определен конструктор:
Person(std::string p_name, unsigned p_age) { name = p_name; age = p_age; std::cout << "Person has been created" << std::endl; }
По сути конструктор представляет функцию, которая может принимать параметры и которая должна называться по имени класса. В данном случае конструктор принимает два параметра и передает их значения полям name и age, а затем выводит сообщение о создании объекта.
Если мы определяем свой конструктор, то компилятор больше не создает конструктор по умолчанию. И при создании объекта нам надо обязательно вызвать определенный нами конструктор.
Вызов конструктора получает значения для параметров и возвращает объект класса:
Person tom("Tom", 38);
После этого вызова у объекта person для поля name будет определено значение "Tom", а для поля age - значение 38. Вполедствии мы также сможем обращаться к этим полям и переустанавливать их значения.
В качестве альтернативы для создания объекта можно использовать инициализатор в фигурных скобках:
Person tom{"Tom", 38};
Тажке можно присвоить объекту результат вызова конструктора:
Person tom = Person("Tom", 38);
По сути она будет эквивалетна предыдущей.
Консольный вывод определенной выше программы:
Person has been created Name: Tom Age: 38
Конструкторы облегчают нам создание нескольких объектов, которые должны иметь разные значения:
#include <iostream> class Person { public: std::string name; unsigned age; void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } Person(std::string p_name, unsigned p_age) { name = p_name; age = p_age; std::cout << "Person has been created" << std::endl; } }; int main() { Person tom{"Tom", 38}; Person bob{"Bob", 42}; Person sam{"Sam", 25}; tom.print(); bob.print(); sam.print(); }
Здесь создаем три разных объекта класса Person (условно трех разных людей), и соответственно в данном случае консольный вывод будет следующим:
Person has been created Person has been created Person has been created Name: Tom Age: 38 Name: Bob Age: 42 Name: Sam Age: 25
Подобным образом мы можем определить несколько конструкторов и затем их использовать:
#include <iostream> class Person { std::string name{}; unsigned age{}; public: void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } Person(std::string p_name, unsigned p_age) { name = p_name; age = p_age; } Person(std::string p_name) { name = p_name; age = 18; } Person() { name = "Undefined"; age = 18; } }; int main() { Person tom{"Tom", 38}; // вызываем конструктор Person(std::string p_name, unsigned p_age) Person bob{"Bob"}; // вызываем конструктор Person(std::string p_name) Person sam; // вызываем конструктор Person() tom.print(); bob.print(); sam.print(); }
В классе Person определено три конструктора, и в функции все эти конструкторы используются для создания объектов:
Name: Tom Age: 38 Name: Bob Age: 18 Name: Undefined Age: 18
Хотя пример выше прекрасно работает, однако мы можем заметить, что все три конструктора выполняют фактически одни и те же действия - устанавливают значения переменных name и age. И в C++ можем сократить их определения, вызова из одного конструктора другой и тем самым уменьшить объем кода:
#include <iostream> class Person { std::string name{}; unsigned age{}; public: void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } Person(std::string p_name, unsigned p_age) { name = p_name; age = p_age; std::cout << "First constructor" << std::endl; } Person(std::string p_name): Person(p_name, 18) // вызов первого конструктора { std::cout << "Second constructor" << std::endl; } Person(): Person(std::string("Undefined")) // вызов второго конструктора { std::cout << "Third constructor" << std::endl; } }; int main() { Person sam; // вызываем конструктор Person() sam.print(); }
Запись Person(string p_name): Person(p_name, 18)
представляет вызов конструктора, которому передается значение параметра p_name и число 18. То есть второй
конструктор делегирует действия по инициализации переменных первому конструктору. При этом второй конструктор может дополнительно определять какие-то свои действия.
Таким образом, следующее создание объекта
Person sam;
будет использовать третий конструктор, который в свою очередь вызывает второй конструктор, а тот обращается к первому конструктору.
Данная техника еще называется делегированием конструктора, поскольку мы делегируем инициализацию другому конструктору.
Как и другие функции, конструкторы могут иметь параметры по умолчанию:
#include <iostream> class Person { std::string name; unsigned age; public: // передаем значения по умолчанию Person(std::string p_name = "Undefined", unsigned p_age = 18) { name = p_name; age = p_age; } void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } }; int main() { Person tom{"Tom", 38}; Person bob{"Bob"}; Person sam; tom.print(); // Name: Tom Age: 38 bob.print(); // Name: Bob Age: 18 sam.print(); // Name: Undefined Age: 18 }
В теле конструктора мы можем передать значения переменным класса. Однако константы требуют особого отношения. Например, вначале определим следующий класс:
class Person { const std::string name; unsigned age{}; public: void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } Person(std::string p_name, unsigned p_age) { name = p_name; age = p_age; } };
Этот класс не будет компилироваться из-за отсутствия инициализации константы name. Хотя ее значение устанавливается в конструкторе, но к моменту, когда инструкции из тела конструктора начнут выполняться, константы уже должны быть инициализированы. И для этого необходимо использовать списки инициализации:
#include <iostream> class Person { const std::string name; unsigned age{}; public: void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } Person(std::string p_name, unsigned p_age) : name{p_name} { age = p_age; } }; int main() { Person tom{"Tom", 38}; tom.print(); // Name: Tom Age: 38 }
Списки инициализации представляют перечисления инициализаторов для каждой из переменных и констант через двоеточие после списка параметров конструктора:
Person(std::string p_name, unsigned p_age) : name{p_name}
Здесь выражение name{p_name}
позволяет инициализировать константу значением параметра p_name. Здесь значение помещается в фигурные скобки, но также можно использовать кргулые:
Person(std::string p_name, unsigned p_age) : name(p_name)
Списки инициализации пободным образом можно использовать и для присвоения значений переменным:
class Person { const std::string name; unsigned age; public: void print() { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } Person(std::string p_name, unsigned p_age) : name(p_name), age(p_age) { } };
При использовании списков инициализации важно учитывать, что передача значений должна идти в том порядке, в котором константы и переменные определены в классе. То есть в данном случае в классе сначала определена константа name, а потом переменная age. Соответственно в таком же порядке идет передача им значений. Поэтому при добавлении дополнительных полей или изменения порядка существующих придется следить, чтобы все инициализировалось в належащем порядке.