Jak działają konstruktory w C++
W tym artykule skupimy się na konstruktorach. Będziemy mówić o ich użyciu oraz o typach konstruktorów w C++, obrazując to wszystko przykładami.
Czym są konstruktory?
Są to specjalne metody klasy, które inicjują dane składowe obiektu. Konstruktory są automatycznie uruchamiane dla każdego obiektu, a w programowaniu obiektowym są publiczne i często nazywają się tak samo, jak klasa. Nie mają one też typu zwracanego.
Przyjrzymy się teraz prostej klasie z takim konstruktorem.
#include <iostream>
using namespace std;
class Example
{
public: //Access Specifier
Example() // Constructor
{
cout << "I am in a constructor"; //Body of the constructor
}
};
int main()
{
Example ExampleObject; // Create an object(ExampleObject) of
//class(Example), which will call the
//constructor.
return 0;
}
Klasa: Example
Konstruktor: Example()
Obiekt klasy: ExampleObject
Nasz program ma klasę o nazwie Example
, co oznacza, że jej konstruktor będzie miał taką samą nazwę, czyli Example()
. Kiedy wywołujemy obiekt (ExampleObject
) w funkcji main
, to konstruktor jest automatycznie uruchamiany i daje następujący wynik:
I am in a constructor
Konstruktory można również definiować poza klasą, korzystając z jej nazwy i operatora zakresu (::
).
#include <iostream>
using namespace std;
class Example
{
public:
Example(); // Constructor declaration
};
Example::Example() // Constructor Definition
{
cout << "I am in a constructor"; //Body of the constructor
};
int main()
{
Example ExampleObject;
return 0;
}
Otrzymamy wtedy taki sam wynik, jak przy pierwszym programie.
I am in a constructor
Kiedy tworzymy obiekt, to wykonywany jest jeden z jego konstruktorów. Jeśli jednak nie mamy żadnego konstruktora, to wygeneruje go kompilator. Nie będzie on jednak przyjmował żadnego argumentu.
Parametry w konstruktorze
Parametr to specjalny rodzaj zmiennej, której używa się do przekazywania argumentów do klasy, lub funkcji. Konstruktory mogą przyjmować parametry w taki sam sposób, jak zwykła funkcja, co jest kluczowe dla konfiguracji początkowych wartości.
#include <iostream>
using namespace std;
class Player
{
public: // Access specifier
string name; // Attribute
string club; // Attribute
int age; // Attribute
//Constructor with parameters
Player(string x, string y, int z): name(x), club(y), age(z){}
};
int main()
{
Player p1("Messi", "Barcelona", 33); //Object for Constructor
Player p2("Ronaldo", "Juventus", 35); //Object for Constructor
// Printing values
cout << p1.name<< " " << p1.club << " " << p1.age << "\n";
cout << p2.name<< " " << p2.club << " " << p2.age << "\n";
return 0;
}
Powyższa klasa ma następujące atrybuty: name
, club
i age
oraz konstruktor z parametrami. Tworzymy obiekt klasy w głównej funkcji i tam też wywołujemy konstruktor. Co więcej, przekazujemy do konstruktora analogiczne dane, aby zainicjalizować atrybuty.
Output:
Messi Barcelona 33
Ronaldo Juventus 35
Często też widzimy, że pola klasy są inicjalizowane wewnątrz ciała konstruktora tak jak poniżej. Nie jest to niestety dobra praktyka.
// Not the best practice!
fruit()
{
name="apple"
}
Dlaczego używamy konstruktorów?
Konstruktorów używa się do inicjalizowania pól klasy, szczególnie tych, które są wymagane do działania metod. Na przykład, jeśli mamy obiekt dla koła, to ważne jest, aby podać jego promień, aby metody, takie jak ShowArea()
, mogły zadziałać bez błędów.
Konstruktor zainicjuje wartość i sprawi, że dane podczas wykonywania danej metody będą prawidłowe.
#include <iostream>
using namespace std;
class Area
{
private:
int radius;
public:
Area(): radius(10.0){ } //Constructor
void FindRadius()
{
cout << "Enter radius: ";
cin >> radius;
}
float FindArea()
{
return (3.14 * radius * radius);
}
void ShowArea(float rad)
{
cout << "Area of circle with user input: " << rad;
};
int main()
{
Area A1, A2;
float rad;
A1.FindRadius();
rad = A1.FindArea();
A1.ShowArea(rad);
cout << endl << "Area of circle when radius is automatically
initialized: ";
rad = A2.FindArea();
A2.ShowArea(rad);
return 0;
}
W naszym programie mamy dwa obiekty klasy Area: A1
oraz A2
. Następnie wywołujemy funkcję FindRadius()
, a ta potrzebuje od użytkownika informacji nt. promienia koła. Dzięki niej ShowArea()
może wydrukować powierzchnię okręgu.
Następnie widzimy, że wartość przypisana w konstruktorze (radius=10
) to nasz promień. Wartość ta zostanie użyta do wyliczenia powierzchni.
Enter radius: 5
Area of circle with user input: 78.5
Area of circle when radius is automatically initialized: 314
Dobra praktyka: zawsze podawaj wartości domyślne dla pól.
Główne typy konstruktorów w C++
- Domyślny konstruktor
- Konstruktor
- Konstruktor kopiujący
Domyślny konstruktor
Ten rodzaj konstruktora nie przyjmuje żadnych argumentów. Nie mają one też żadnych parametrów, ale mogą mieć parametry z wartościami domyślnymi. Pierwszy program z tego artykułu to przykład z domyślnym konstruktorem.
#include <iostream>
using namespace std;
class Year
{
public:
int date;
Year(): date(2020) {} //Default Constructor
};
int main()
{
Year y; //Object for constructor
cout << y.date;
}
Klasa Year
ma domyślny konstruktor, który gdy zostaje wywołany, ustawia wartość date
na 2020
. Kiedy zmienna date
z klasy Year
jest wywoływana i drukowana, to daje następujący wynik:
2020
Konstruktor
Są to konstruktory posiadające parametry. Przekazując odpowiednie wartości, możemy przypisywać różne wartości do składowych obiektów. Program pokazany w sekcji „Parametry w konstruktorze” to właśnie taki konstruktor.
#include <iostream>
using namespace std;
class Player
{
public: // Access specifier
string name; // Attribute
Player(string x): name(x) {} // Constructor with parameter
};
int main()
{
Player p1("Messi"); //Object for Constructor
Player p2("Ronaldo");
Player p3("Bale");
// Print values
cout << p1.name<<"\n";
cout << p2.name<<"\n";
cout << p3.name<<"\n";
return 0;
}
Tutaj parametr konstruktora to ciąg znaków zawierający nazwę gracza. Gdy wartość zostaje przekazana, ciąg znaków zostaje przypisany do zdefiniowanej zmiennej.
Output:
Messi
Ronaldo
Bale
Konstruktor kopiujący
Tego konstruktora używa się do tworzenia kopii istniejących już obiektów klas.
Dla klasy Example
z konstruktorem Example()
i zdefiniowanym wcześniej obiektem E1
możemy utworzyć obiekt E2
będący dokładną kopią obiektu E1
:
class Example
{
Example()
{
cout<<"Object E1"
}
}
int main()
{
Example E1;
Example E2(A1);
//OR
Example E2 = E1; //Copies content of E1 to E2 if there is any
}
W tym przypadku obiekt E2
zawiera te same wartości, co obiekt E1
. Ułatwia to sprawę, gdy zachodzi potrzeba duplikowania wartości, ponieważ wszystkie klasy mają domyślnie wbudowany konstruktor kopiujący.
Przeciążanie konstruktora w C++
Konstruktory w C++ można przeciążać. Przeciążone konstruktory występują, gdy domyślny i zwykły konstruktor zostają zdefiniowane w klasie - jeden ma parametr, a drugi nie. Konstruktory można również przeciążać, gdy posiadamy ich więcej i różnią się od siebie parametrami.
#include <iostream>
using namespace std;
class Player
{
public:
int no;
string name;
// first constructor
Player(int x): no(x)
{
name = "None";
cout<<no<<" "<<name;
}
// second constructor
Player(int x, string str): no(x), name(str)
{
cout<<endl<<no<<" "<<name;
}
};
int main()
{
// Player P1 initialized with no 105 and name None
Player P1(105);
// Player P2initialized with roll no 11 and name Bale
Player P2(11, "Bale");
}
W powyższym kodzie mamy dwa konstruktory z różnymi parametrami, co powoduje ich przeciążenie.
Output:
105 None
11 Bale
Gdy jawnie definiujemy dowolny konstruktor bez zdefiniowania konstruktora domyślnego, to kompilator go nam nie stworzy. W takim przypadku będziemy musieli samodzielnie dokonać definicji konstruktora domyślnego.
Oryginał tekstu w języku angielskim możesz przeczytać tutaj.