Benutzerdefinierte Konstruktordefinitionen in C++
Undefiniertes Verhalten ohne Initialisierung
In dem folgenden Programm wird der Wert nicht-initialisierter Objekte (»x« und »y«) ausgegeben. Dies führt zu undefiniertem Verhalten.
main.cpp
#include <initializer_list>
#include <iostream>
#include <ostream>struct pair
{ double x;
double y;void print()
{ ::std::cout << this->x << '\n';
::std::cout << this->y << '\n'; }};int main()
{ pair p;
p.print(); }Protokoll
0
0- Zitat *
- “When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced. If an indeterminate value is produced by an evaluation, the behavior is undefined”
- C++ 2016, 8.6p12; C++ 2017, 11.6p12
Initialisierung mit Hilfe einer Elementfunktion
Im nächsten Beispielprogramm wird das Objekt mit einer Elementfunktion »init()« auf einen bestimmten Anfangszustand gesetzt. Das Programm zeigt nun kein undefiniertes Verhalten mehr.
main.cpp
#include <initializer_list>
#include <iostream>
#include <ostream>struct pair
{ double x;
double y;void set
( double const x,
double const y )
{ this->x = x;
this->y = y; }void init()
{ this->set( 0., 0. ); }void print()
{ ::std::cout << this->x << '\n';
::std::cout << this->y << '\n'; }};int main()
{ pair p;
p.init();
p.print(); }Protokoll
0
0
Initialisierung mit Hilfe eines Konstruktors
Im vorigen Beispielprogramm mußte der Klient die Elementfunktion »init()« aufrufen, um ein Objekt zu initialisieren, also auf einen wohldefinierten Anfangszustand zu setzen. Dies verkompliziert den Klienten aber und könnte auch einmal vergessen werden.
Im folgenden Beispiel wird daher ein Konstruktor definiert, der automatisch zu Initialisierung eines Objekts eingesetzt wird, um die gleiche Aufgabe auf elegantere Weise zu erledigen.
main.cpp
#include <initializer_list>
#include <iostream>
#include <ostream>struct pair
{ double x;
double y;pair()
{ ::std::cout << "Konstruktor\n";
this->set( 0., 0. ); }void set
( double const x,
double const y )
{ this->x = x;
this->y = y; }void print()
{ ::std::cout << this->x << '\n';
::std::cout << this->y << '\n'; }};int main()
{ pair p;
p.print(); }Protokoll
Konstruktor
0
0
Der Konstruktor hat keinen eigenen Namen. Seine Definition ähnelt einer Funktionsdefinition. Die Konstruktordefinition hat aber keinen Ergebnistyp, und an Stelle eines eigenen Namens wird der Name der Klasse geschrieben, zu welcher der Konstruktor gehört.
Wie eine Funktionsdefinition enthält auch eine Konstruktordefinition ein Paar runder Klammern (oben »()«) und einen Block (der mit einer geschweiften Klammer beginnt und endet).
Wie die Ausgabe »Konstruktor« erkennen läßt, wird der Konstruktor bei der Erzeugung des Objekts (in »main()«) aktiviert.
Implizit definierte Konstruktoren
Wenn vom Benutzer ein parameterloser Konstruktor definiert wird, wird kein parameterloser Konstruktor mehr implizit definiert. Es wird aber weiterhin ein Kopierkonstruktor stillschweigend definiert.
main.cpp
#include <initializer_list>
#include <iostream>
#include <ostream>struct pair
{ double x;
double y;pair()
{ this->set( 0., 0. ); }void set( double const x, double const y )
{ this->x = x; this->y = y; }void print()
{ ::std::cout << this->x << '\n' << this->y << '\n'; }};int main()
{ pair p; p.set( 2, 3 );
pair q( p );
q.print(); }Protokoll
2
3
Der implizit definierte Fehlkonstruktor führt dieselben Initialisierungen herbei wie ein benutzerdefinierter Fehlkonstruktor mit einem leeren Rumpf. Man kann sich den implizit definierten Fehlkonstruktor als vorstellen, wie den benutzerdefinierten Fehlkonstruktor »pair(){}« im folgenden Programm (das wieder undefiniertes Verhalten hat).
main.cpp
#include <initializer_list>
#include <iostream>
#include <ostream>struct pair
{ double x;
double y;pair(){}
void set( double const x, double const y )
{ this->x = x; this->y = y; }void print()
{ ::std::cout << this->x << '\n' << this->y << '\n'; }};int main()
{ pair p;
p.print(); }Protokoll
0
0
Die Standardrichtilinien empfehlen die Initialisierung der Felder am Orte ihrer Deklaration.
C.45: Don't define a default constructor that only initializes data members; use in-class member initializers instead
- Tabelle
.- Benutzer Fehl- Kopier- Zuweisung
| deklariert: konstruktor konstruktor
|
|
|
| nichts vorgegeben vorgegeben vorgegeben
|
| Fehl- benutzer- vorgegeben vorgegeben
| konstruktor deklariert
|
| Kopier- nicht benutzer- vorgegeben*
| konstruktor deklariert deklariert
|
| anderen nicht vorgegeben vorgegeben
| Konstruktor deklariert
|
| Zuweisung vorgegeben vorgegeben* benutzer-
| deklariert
'-
* = veraltend
Namen von Feldern
Auch wenn dies nicht immer verboten ist, sollte der Name eines Feldes im allgemeinen nicht mit dem Namen der Klasse übereinstimmen.
- Quellenangabe *
- “if class T has a user-declared constructor (12.1), every non-static data member of class T shall have a name different from T. ”, 2015, 9.2.15
In dem speziellen Fall, daß eine Klasse keinen benutzer-deklarierten Konstruktor hat, darf der Name eines Feldes jedoch mit dem Namen der Klassen übereinstimmen, wie das folgende Beispiel zeigt, in dem wir auch gleich noch eine lokale Variable der Funktion »main« wie die Klasse benannt haben.
main.cpp
#include <iostream>
#include <ostream>
#include <string>struct entity { ::std::string entity; };
int main()
{ entity entity; ::std::cout << '"' << entity.entity << '"' << '\n'; }::std::cout
""
Diese Erlaubnis ist wohl nur zur Kompatibilität mit der Programmiersprache C eingeführt worden. Es ist aber nicht erlaubt, im obigen Programm statt »entity.entity« zu schreiben: »entity.::entity::entity«, vermutlich da dies nicht mehr zur Kompatibilität mit C benötigt wird.
Sobald eine Klasse einen benutzerdefinierten Konstruktor hat, muß jeder nicht-statische Eintrag der Klasse einen anderen Namen als die Klasse haben.
In diesem Kurs werden gleichlautende Namen gelegentlich verwendet, um zu illustrieren, daß Namen in Abhängigkeit von ihrem Kontext interpretiert werden. So steht in dem obigen Programm in »entity entity« und in »entity.entity« das Wort »entity« jeweils für eine andere Entität (Sache).
Übungsaufgaben
/ Übungsaufgabe
Definieren Sie eine Klasse, die einen Konstruktor hat, der die Zeile »Konstruktor« ausgibt, mit einer Klassendefinition, die möglichst wenige lexikalische Einheiten umfassen soll.
Legen Sie im Hauptprogramm einen Vektor an, der Objekte der von Ihnen definierte Klasse als Komponenten haben kann. Bei der Definition soll der Vektor zunächst mit einer Größe von drei angelegt werden. Fügen Sie dann zehn Objekte der von Ihnen definierte Klasse zu dem Vektor hinzu und zählen Sie wie oft dann insgesamt der von Ihnen definierte Konstruktor aufgerufen wurde.
Ausblick
(geplante Inhalte späterer Lektionen)
main.cpp
#include <iostream>
#include <initializer_list>
using namespace std;
template< class T >
static void say( T const & s ){ ::std::cout << s << '\n'; }struct S
{
S() { say( "A" ); }explicit S( int ) { say( "B" ); }
explicit S( double ) { say( "C" ); }
S( initializer_list<int> ) { say( "E" ); }};
int main()
{ S{};
S(1);
//S{1.2};
S{1}; }
- transcript
A
B
E
main.cpp
#include <iostream>
#include <initializer_list>
using namespace std;
template< class T >
static void say( T const & s ){ ::std::cout << s << '\n'; }struct S
{
explicit S( double ) { say( "C" ); }S( initializer_list<int> ) { say( "E" ); }};
int main()
{ S{1.2}; }
- transcript
main.cpp: In function 'int main()':
main.cpp:27:8: error: narrowing conversion of '1.2e+0' from 'double' to 'int' inside { } [-Wnarrowing]
{ S{1.2}; }
^