Конструкторы представляют специальную функцию, которая имеет то же имя, что и класс, которая не возвращает никакого значения и которая позволяют инициалилизировать объект класса во время го создания и таким образом гарантировать, что поля класса будут иметь определенные значения. При каждом создании нового объекта класса вызывается конструктор класса.
В прошлой теме был разработан следующий класс:
#include <iostream>
#include <string>
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>
#include <string>
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>
#include <string>
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>
#include <string>
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>
#include <string>
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(std::string p_name): Person(p_name, 18) представляет вызов конструктора, которому передается значение параметра p_name и число 18. То есть второй
конструктор делегирует действия по инициализации переменных первому конструктору. При этом второй конструктор может дополнительно определять какие-то свои действия.
Таким образом, следующее создание объекта
Person sam;
будет использовать третий конструктор, который в свою очередь вызывает второй конструктор, а тот обращается к первому конструктору.
Данная техника еще называется делегированием конструктора, поскольку мы делегируем инициализацию другому конструктору.
Как и другие функции, конструкторы могут иметь параметры по умолчанию:
#include <iostream>
#include <string>
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>
#include <string>
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. Соответственно в таком же порядке идет передача им значений. Поэтому при добавлении дополнительных полей или изменения порядка существующих придется следить, чтобы все инициализировалось в належащем порядке.