Конструкторы и инициализация объектов

Последнее обновление: 09.03.2023

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

В прошлой теме был разработан следующий класс:

#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. Соответственно в таком же порядке идет передача им значений. Поэтому при добавлении дополнительных полей или изменения порядка существующих придется следить, чтобы все инициализировалось в належащем порядке.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850