Конструкторы и инициализация объектов
В прошлой теме был разработан следующий класс:
1
2
3
4
5
6
7
8
9
class Person
{
public:
string name;
int age;
void move() {
cout << name << " is moving"<< endl;
}
};
И мы можем установить значения для переменных класса Person, можем получить их значения во внешние переменные. Однако если мы попробуем получить значения переменных name и age до их установки, то результаты будут неопределенными:
1
2
Person person;
cout << "Name: " << person.name << "\tAge: " << person.age << endl;
Чтобы избежать подобной ситуации применяются специальные функции инициализации или конструкторы. Они позволяют инициализировать объект класса. Так, изменим код программы следующим образом:
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
#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;
class Person
{
public:
string name;
int age;
Person(string n, int a)
{
name = n; age = a;
}
void move()
{
cout << name << " is moving" << endl;
}
};
int main()
{
Person person = Person("Tom", 22);
cout << "Name: " << person.name << "\tAge: " << person.age << endl;
person.name = "Bob";
person.move();
return 0;
}
Теперь в классе Person определен конструктор:
1
2
3
4
Person(string n, int a)
{
name = n; age = a;
}
По сути конструктор представляет функцию, которая может принимать параметры и которая должна называться по имени класса. В данном случае конструктор принимает два параметра и передает их значения полям name и age.
Если в классе определены конструкторы, то при создании объекта этого класса необходимо вызвать один из его конструкторов.
Вызов конструктора получает значения для параметров и возвращает объект класса:
1
Person person = Person("Tom", 22);
После этого вызова у объекта person для поля name будет определено значение "Tom", а для поля age - значение 22. Вполедствии мы также сможем обращаться к этим полям и переустанавливать их значения.
Тажке можно использовать сокращенную форму инициализации:
1
Person person("Tom", 22);
По сути она будет эквивалетна предыдущей.
Консольный вывод определенной выше программы:
Name: Tom Age: 22
Bob is moving
Подобным образом мы можем определить несколько конструкторов и затем их использовать:
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
#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;
class Person
{
public:
string name;
int age;
Person(string n, int a)
{
name = n;
age = a;
}
Person(string n)
{
name = n;
age = 18;
}
Person()
{
name = "Bob";
age = 18;
}
void move()
{
cout << name << " is moving" << endl;
}
};
int main()
{
Person tom("Tom", 22);
cout << "Name: " << tom.name << "\tAge: " << tom.age << endl;
Person sam("Sam");
cout << "Name: " << sam.name << "\tAge: " << sam.age << endl;
Person bob = Person();
cout << "Name: " << bob.name << "\tAge: " << bob.age << endl;
return 0;
}
В классе Person определено три конструктора, и в функции все эти конструкторы используются для создания объектов:
Name: Tom Age: 22
Name: Sam Age: 18
Name: Bob Age: 18
Хотя пример выше прекрасно работает, однако мы можем заметить, что все три конструктора выполняют фактически одни и те же действия - устанавливают значения переменных name и age. И в C++ можем сократить их определения, вызова из одного конструктора другой и тем самым уменьшить объем кода:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person
{
public:
string name;
int age;
Person(string n, int a)
{
name = n; age = a;
cout << "First constructor" << endl;
}
Person(string n): Person(n, 18) // вызов первого конструктора
{
cout << "Second constructor" << endl;
}
Person() : Person("Bob") // вызов второго конструктора
{
cout << "Third constructor" << endl;
}
void move()
{
cout << name << " is moving" << endl;
}
};
Запись Person(string n): Person(n, 18) представляет вызов конструктора, которому передается значение параметра n и число 18. То есть второй конструктор делегирует действия по инициализации переменных первому конструктору. При этом второй конструктор может дополнительно определять какие-то свои действия.
Таким образом, следующее создание объекта
1
Person bob = Person();
будет использовать третий конструктор, который в свою очередь вызывает второй конструктор, а тот обращается к первому конструктору.
Инициализация констант и ссылок
В теле конструктора мы можем передать значения переменным класса. Однако константы и ссылки требуют особого отношения. Например, вначале определим следующий класс:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
public:
const string name;
int &ageRef;
int age;
Person(string n, int a)
{
name = n; age = a; ageRef = age;
}
Person(string n)
{
name = n; age = 18; ageRef = age;
}
void move()
{
cout << name << " is moving" << endl;
}
};
Этот класс не будет компилироваться, так как здесь есть две ошибки - отсутствие инициализации константы name и ссылки ageRef. Хотяя их значения устанавливаются в конструкторе, но к моменту, когда код инструкции из тела конструктора начнут выполняться, константы и ссылки уже должны быть инициализированы. И для этого необходимо использовать списки инициализации:
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
#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;
class Person
{
public:
const string name;
int &ageRef;
int age;
Person(string n, int a) : name(n), age(a), ageRef(age)
{
}
Person(string n) : name(n), age(18), ageRef(age)
{
}
void move()
{
cout << name << " is moving" << endl;
}
};
int main()
{
Person tom("Tom", 22);
cout << "Name: " << tom.name << "\tAge: " << tom.age << "\t AgeRef: " << tom.ageRef << endl;
return 0;
}
Списки инициализации представляют перечисления инициализаторов для каждой из переменных и констант через двоеточие после списка параметров конструктора:
1
2
3
Person(string n, int a) : name(n), age(a), ageRef(age)
{
}
Таким образом, все переменные, константы и ссылки получат значение, и никакой ошибки не возникнет.