Parameter | Kursinformationen |
---|---|
Veranstaltung: | @config.lecture |
Semester | @config.semester |
Hochschule: | Technische Universität Freiberg |
Inhalte: | Klassen und Objekte |
**Link auf Repository: ** | https://github.com/TUBAF-IfI-LiaScript/VL_EAVD/blob/master/04_Funktionen.md |
Autoren | @author |
Fragen an die heutige Veranstaltung ...
- Wie strukturieren wir unseren Code?
- Was sind Objekte?
- Welche Aufgabe erfüllt ein Konstruktor?
- Was geschieht, wenn kein expliziter Konstruktor durch den Entwickler vorgesehen wurde?
- Wann ist ein Dekonstruktor erforderlich?
- Was bedeutet das "Überladen" von Funktionen?
- Nach welchen Kriterien werden überladene Funktionen differenziert?
- Während der Klausur können Sie "alle Hilfsmittel aus Papier" verwenden!
- Im OPAL finden sich Klausurbeispiele.
Beispielhafte Klausuraufgabe
Die Zustimmung (in Prozent) für die Verwendung der künstlichen Intelligenz im Pflegebereich unter der Bevölkerung von Mauritius und Réunion soll vergleichend betrachtet werden. Die Ergebnisse der Umfragen für die Jahre 2010 bis 2020 (je ein Wert pro Jahr) in zwei Arrays erfasst werden (je ein Array pro Insel) und in einem Programm ausgewertet werden.
- Für beide Inseln soll aus den in Arrays erfassten Werten je ein Mittelwert berechnet werden. Schreiben Sie dazu eine Funktion, die ein Array übergeben bekommt und einen Mittelwert als ein Ergebnis an die main-Funktion zurück liefert. Rufen Sie die Funktion in der main-Funktion für jedes beider Arrays auf und geben Sie die Mittelwerte in der main-Funktion aus.
- Schreiben Sie eine weitere Funktion, die die korrespondierenden Werte beider Arrays miteinander vergleicht. Geben Sie für jedes Jahr aus, auf welcher Insel die Zustimmung größer war, bei den gleichen Werte ist eine entsprechende Meldung auszugeben. Rufen Sie die Funktion in der main-Funktion auf.
- In der main()-Funktion sind die Werte von der Console einzulesen und in die Arrays zu speichern.
Denken Sie mal über eine Lösung nach, wie sprechen in der kommenden Veranstaltung darüber.
{{0-2}}
Aufgabe: Nehmen wir an, dass Sie eine statistische Untersuchung über einem Datensatz vornehmen und zum Beispiel für Mitarbeiter die Gehaltsentwicklung in Abhängigkeit vom Alter darstellen wollen.
Welche zwei Elemente machen eine solche Untersuchung aus?
-
Daten
- Name
- Geburtsdatum
- Gehalt
- ...
-
Funktionen
- Alter bestimmen
- Daten ausgeben
- ...
Wir entwerfen also eine ganze Sammlung von Funktionen wie zum Beispiel für alterbestimmen()
:
int alterbestimmen(int tag, int monat, int jahr,
int akt_tag, int akt_monat, int akt_jahr)
{
//TODO
}
int main(){
int tag, monat, jahr;
int akt_tag, akt_monat, akt_jahr;
int alter=alterbestimmen(tag,monat,jahr,akt_tag,akt_monat,akt_jahr);
}
Was gefällt ihnen an diesem Ansatz nicht?
{{1-2}}
- lange Parameterliste bei der Funktionen
- viele Variablen
- der inhaltliche Zusammenhang der Daten ist nur schwer zu erkennen
Wir brauchen eine neue Idee, um die Daten zu strukturieren!
Mit Strukturen oder structs
werden zusammengehörige Variablen unterschiedlicher Datentypen und
Bedeutung in einem Konstrukt zusammengefasst. Damit wird für den Entwickler der
Zusammenhang deutlich. Die Variablen der Struktur werden als Komponenten (engl.
members) bezeichnet.
Beispiele:
struct datum
{
int tag;
char monat[10];
int jahr;
};
struct Student
{
int Matrikel;
char name[20];
char vorname[25];
}; // <- Hier steht ein Semikolon!
Und wie erzeuge ich Variablen dieses erweiterten Types, wie werden diese initialisiert und wie kann ich auf die einzelnen Komponenten zugreifen?
Der Bespiel zeigt, dass die Definition der Variable unmittelbar nach der
struct
-Definition oder mit einer gesonderten Anweisung mit einer vollständigen,
partiellen Initialisierung bzw. ohne Initialisierung erfolgen kann.
Die nachträgliche Veränderung einzelner Komponenten ist über Zugriff mit Hilfe des Punkt-Operators möglich.
#include <iostream>
#include <cstring>
struct datum // <- Deklaration
{
int tag;
char monat[10];
int jahr;
} geburtstag_1 = {18, "April", 1986}; // <- Initialisierung
// globale Variable geburtstag_1
void print_date(struct datum day){
std::cout << "Person A wurde am "
<< day.tag << ". "
<< day.monat << " "
<< day.jahr << " geboren."
<< std::endl;
}
int main() {
struct datum geburtstag_2 = {}; // <- Initialisierung
// Variable geburtstag_2
geburtstag_2.tag = 13; // Zuweisungen
geburtstag_2.jahr =1803;
strcpy(geburtstag_2.monat, "April");
// Unvollstaendige Initialisierung
struct datum geburtstag_3 ={.monat = "September"}; // <- partielle Initialisierung
print_date(geburtstag_1);
print_date(geburtstag_2);
print_date(geburtstag_3);
return 0;
}
@LIA.evalWithDebug(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Zusammengesetzte Datentypen machen die Anwendung von Vergleichsoperatoren schwieriger - bzw. wir müssen sie selbst definieren.
Im nachfolgenden Beispiel werden zur Überprüfung der Gleichheit die tag
und
monat
verglichen. Der Vergleich wird dadurch vereinfacht, dass
wir für die Repräsentation des Monats ein int
verwenden. Damit entfällt
aufwändigerer Vergleich der char arrays
.
#include <iostream>
#include <cstring>
struct datum // <- Deklaration
{
int tag;
int monat;
int jahr;
};
int main() {
struct datum person_1 = {10, 1, 2013};
struct datum person_2 = {10, 3, 1956};
if ((person_1.tag == person_2.tag) && (person_1.monat == person_2.monat))
printf("Oha, der gleiche Geburtstag im Jahr!\n");
else
printf("Ungleiche Geburtstage!\n");
return 0;
}
@LIA.evalWithDebug(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Natürlich lassen sich die beiden erweiterten Datenformate auf der Basis von
struct
und Arrays miteinander kombinieren.
#include <iostream>
#include <cstring>
struct datum
{
int tag;
int monat;
int jahr;
};
void print_date(struct datum day){
std::cout << "Person A wurde am "
<< day.tag << ". "
<< day.monat << " "
<< day.jahr << " geboren."
<< std::endl;
}
int main() {
struct datum geburtstage [3] = {{18, 4, 1986},
{12, 5, 1820}};
geburtstage[2].tag = 5;
geburtstage[2].monat = 9;
geburtstage[2].jahr = 1905;
for (long unsigned int i=0; i<sizeof(geburtstage)/sizeof(datum); i++)
print_date(geburtstage[i]);
return 0;
}
@LIA.evalWithDebug(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Mit Blick auf unsere Eingangsfragestellung könnte die Lösung also wie folgt aussehen:
struct Datum{ // hier wird ein neuer Datentyp definiert
int tag, monat, jahr;
};
int alterbestimmen(struct Datum geb_datum, struct Datum akt_datum)
{
//TODO
return 0;
}
int main(){
Datum geburtsdatum;
Datum akt_datum; // ... und hier wird eine Variable des
// Typs angelegt
geburtsdatum.tag = 12; // ... und "befüllt"
geburtsdatum.monat = 3;
geburtsdatum.jahr = 1920;
int alter=alterbestimmen(geburtsdatum, akt_datum);
}
Was gefällt ihnen an diesem Ansatz nicht?
{{1-2}}
- die Funktionen sind von dem neuen Datentyp abhängig gehören aber trotzdem nicht zusammen
- es fehlt eine Überprüfung der Einträge für die Gültigkeit unserer Datumsangaben
Die objektorientierte Programmierung (OOP) ist ein Programmierparadigma, das auf dem Konzept der "Objekte" basiert, die Daten und Code enthalten können: Daten in Form von Feldern (oft als Attribute oder Eigenschaften bekannt) und Code in Form von Prozeduren (oft als Methoden bekannt).
struct Datum{
int tag,monat,jahr;
int alterbestimmen(Datum akt_datum)
{
//hier sind tag, monat und jahr bereits bekannt
//TODO
}
}
int main(){
Datum geburtstdatum;
Datum akt_datum;
int alter=geburtstdatum.alterbestimmen(akt_datum);
}
C++ sieht vor als Datentyp für Objekte struct
- und class
- Definitionen. Der Unterschied wird später geklärt, vorerst verwenden wird nur die class
- Definitionen.
class Datum{
public:
int tag,monat,jahr;
int alterbestimmen(Datum akt_datum)
{
//hier sind tag, monat und jahr bereits bekannt
//TODO
}
}
int main(){
Datum geburtstdatum;
Datum akt_datum;
int alter=geburtstdatum.alterbestimmen(akt_datum);
}
Ein Merkmal von Objekten ist, dass die eigenen Prozeduren eines Objekts auf die Datenfelder seiner selbst zugreifen und diese oft verändern können - Objekte haben somit eine Vorstellung davon oder von sich selbst :-).
Ein OOP-Computerprogramm kombiniert Objekt und lässt sie interagieren. Viele der am weitesten verbreiteten Programmiersprachen (wie C++, Java, Python usw.) sind Multi-Paradigma-Sprachen und unterstützen mehr oder weniger objektorientierte Programmierung, typischerweise in Kombination mit imperativer, prozeduraler Programmierung.
{{1-2}}
main Farm Animal
+-----------------------+ +--------------------------+ +--------------------+
| Farm JohnsFarm; |->| Animal myAnimals[]; |-> | string name; |
| Farm PetersFarm; | | checkAnimalsPosition(); | | ... |
| ... | | feedAnimals(); |
| getAnimalStatistics(); | Building
| ... | +--------------------+
| Farmbuilding buildings[];|-> | int purpose |
| startScanning() |
| ... |
Wir erzeugen ausgehend von unserem Bauplan verschiedene Instanzen / Objekte vom Typ Animals
. Jede hat den gleichen Funktionsumfang, aber unterschiedliche Daten.
**Merke: ** Unter einer Klasse versteht man in der objektorientierten Programmierung ein abstraktes Modell bzw. einen Bauplan für eine Reihe von ähnlichen Objekten.
C++ eine von der ISO genormte Programmiersprache. Sie wurde ab 1979 von Bjarne Stroustrup bei AT&T als Erweiterung der Programmiersprache C entwickelt. Bezeichnenderweise trug C++ zunächst den Namen "C with classes". C++ erweitert die Abstraktionsmöglichkeiten erlaubt aber trotzdem eine maschinennahe Programmierung. Der Standard definiert auch eine Standardbibliothek, zu der wiederum verschiedene Implementierungen existieren.
Jahr | Entwicklung |
---|---|
197? | Anfang der 70er Jahre entsteht die Programmiersprache C |
... | |
1979 | „C with Classes“ - Klassenkonzept mit Datenkapselung, abgeleitete Klassen, ein strenges Typsystem, Inline-Funktionen und Standard-Argumente |
1983 | "C++" - Überladen von Funktionsnamen und Operatoren, virtuelle Funktionen, Referenzen, Konstanten, eine änderbare Freispeicherverwaltung |
1985 | Referenzversion |
1989 | Version 2.0 - Mehrfachvererbung, abstrakte Klassen, statische Elementfunktionen, konstante Elementfunktionen |
1998 | ISO/IEC 14882:1998 oder C++98 |
... | Kontinuierliche Erweiterungen C++11, C++20, ... |
C++ kombiniert die Effizienz von C mit den Abstraktionsmöglichkeiten der objektorientierten Programmierung. C++ Compiler können C Code überwiegend kompilieren, umgekehrt funktioniert das nicht.
Klasse string
C++ implementiert eine separate Klasse string
-Datentyp (Klasse). In Programmen müssen nur Objekte dieser Klasse angelegt werden. Beim Anlegen eines Objektes muss nicht angegeben werden, wie viele Zeichen darin enthalten werden sollen, eine einfache Zuweisung reicht aus.
#include <iostream>
#include <string>
int main(void) {
std::string hallo_msg="Hallo";
std::cout<< hallo_msg << " hat eine Länge von " << hallo_msg.length() << " Zeichen." << std::endl;
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Schauen Sie auch in die Dokumentation der Klasse
string
http://www.cplusplus.com/reference/string/string/
Arduino Klassen
Die Implementierungen für unseren Mikrocontroller sind auch Objektorientiert. Klassen repräsentieren unserer Hardwarekomponenten und sorgen für deren einfache Verwendung.
#include <OledDisplay.h>
...
Screen.init();
Screen.print("This is a very small display including only 4 lines", true);
Screen.draw(0, 0, 128, 8, BMP);
Screen.clean();
...
Eine Klasse wird in C++ mit dem Schlüsselwort class
definiert und enthält Daten (member variables, Attribute) und Funktionen (member functions, Methoden).
Klassen verschmelzen Daten und Methoden in einem "Objekt" und deklarieren den individuellen Zugriff. Die wichtigste Eigenschaft einer Klasse ist, dass es sich um einen Typ handelt!
class class_name {
access_specifier_1:
member1;
access_specifier_2:
member2;
...
}; // <- Das Semikolon wird gern vergessen
class_name instance_name;
Bezeichnung | Bedeutung |
---|---|
class_name |
Bezeichner für die Klasse - Typ |
instance_name |
Objekte der Klasse class_name - Instanz |
access_specifier |
Zugriffsberechtigung |
Der Hauptteil der Deklaration kann member enthalten, die entweder Daten- oder Funktionsdeklarationen sein können und jeweils einem Zugriffsbezeichner . Ein Zugriffsbezeichner ist eines der folgenden drei Schlüsselwörter: private
, public
oder protected
. Diese Bezeichner ändern die Zugriffsrechte für die member, die ihnen nachfolgen:
private
member einer Klasse sind nur von anderen members derselben Klasse (oder von ihren "Freunden") aus zugänglich.protected
member sind von anderen member derselben Klasse (oder von ihren "Freunden") aus zugänglich, aber auch von Mitgliedern ihrer abgeleiteten Klassen.public
member sind öffentliche member von überall her zugänglich, wo das Objekt sichtbar ist.
Standardmäßig sind alle Members in der Klasse private
!
Merke: Klassen und Strukturen unterscheiden sich unter C++ durch die Default-Zugriffsrechte und die Möglichkeit der Vererbung. Anders als bei
class
sind die member vonstruct
per Default public . Die Veränderung der Zugriffsrechte über die oben genannten Zugriffsbezeichner ist aber ebenfalls möglich.
Im Folgenden fokussieren die Ausführungen Klassen, eine analoge Anwendung mit Strukturen ist aber zumeist möglich.
#include <iostream>
class Datum
{
public:
int tag;
int monat;
int jahr;
void print(){
std::cout << tag << "." << monat <<"." << jahr <<std::endl;
}
};
int main()
{
// 1. Instanz der Klasse, Objekt myDatumA
Datum myDatumA;
myDatumA.tag = 12;
myDatumA.monat = 12;
myDatumA.jahr = 2000;
myDatumA.print();
// 2. Instanz der Klasse, Objekt myDatumB
// alternative Initialisierung
Datum myDatumB = {.tag = 12, .monat = 3, .jahr = 1920};
myDatumB.print();
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Und wie können wir weitere Methoden zu unserer Klasse hinzufügen?
#include <iostream>
class Datum{
public:
int tag;
int monat;
int jahr;
const int monthDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
void print(){
std::cout << tag << "." << monat <<"." << jahr <<std::endl;
}
int istSchaltJahr(){
if (((jahr % 4 == 0) && (jahr % 100 != 0)) || (jahr % 400 == 0)) return 1;
else return 0;
}
int nterTagimJahr(){
int n = tag;
for (int i=0; i<monat - 1; i++){
n += monthDays[i];
}
if (monat > 2) n += istSchaltJahr();
return n;
}
};
int main()
{
Datum myDatumA;
myDatumA.tag = 31;
myDatumA.monat = 12;
myDatumA.jahr = 2000;
myDatumA.print();
std::cout << myDatumA.nterTagimJahr() << "ter Tag im Jahr " << myDatumA.jahr << std::endl;
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Natürlich lassen sich Klassen beliebig miteinander kombinieren, was folgende Beispiel demonstriert.
#include <iostream>
#include <ctime>
#include <string>
class Datum{
public:
int tag;
int monat;
int jahr;
void print(){
std::cout << tag << "." << monat <<"." << jahr <<std::endl;
}
};
class Person{
public:
Datum geburtstag;
std::string name;
void print(){
std::cout << name << ": ";
geburtstag.print();
std::cout <<std::endl;
}
int zumGeburtstagAnrufen() {
time_t t = time(NULL);
tm* tPtr = localtime(&t);
if ((geburtstag.tag == tPtr->tm_mday) &&
(geburtstag.monat == (tPtr->tm_mon + 1))) {
std::cout << "\"Weil heute Dein ... \"" <<std::endl;
return 1;
}
else return -1;
}
};
int main()
{
Person freundA = {.geburtstag = {1, 12, 2022}, .name = "Peter"};
freundA.print();
if (freundA.zumGeburtstagAnrufen() == 1){
std::cout << "Zum Geburtstag gratuliert!" <<std::endl;
}
else{
std::cout << freundA.name << " hat heute nicht Geburtstag" <<std::endl;
}
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Als Datenkapselung bezeichnet man das Verbergen von Implementierungsdetails einer Klasse. Auf die internen Daten kann nicht direkt zugegriffen werden, sondern nur über definierte Schnittstellen, die durch public
-Methoden repräsentiert wird.
- get- und set-Methoden
- andere Methoden
#include <iostream>
using namespace std;
class Datum
{
private:
int tag;
int monat;
int jahr;
public:
void setTag(int _tag){
tag=_tag;
}
void setMonat(int _monat){
monat=_monat;
}
void setJahr(int _jahr){
jahr=_jahr;
}
int getTag(){
return tag;
}
//analog monat und jahr
void ausgabe()
{
cout<<tag<<"."<<monat<<"."<<jahr<<endl;
}
};
int main()
{
Datum datum;
datum.setTag(31);datum.setMonat(12);datum.setJahr(1999);
datum.ausgabe();//jetzt geht es
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Mit der Integration einer Funktion in eine Klasse wird diese zur Methode oder Memberfunktion. Der Mechanismus der Nutzung bleibt der gleiche, es erfolgt der Aufruf, ggf mit Parametern, die Abarbeitung realisiert Berechnungen, Ausgaben usw. und ein optionaler Rückgabewert bzw. geänderte Parameter (bei Call-by-Referenz Aufrufen) werden zurückgegeben.
Worin liegt der technische Unterschied?
#include <iostream>
class Student{
public:
std::string name; // "-"
int alter;
std::string ort;
void ausgabeMethode(){
std::cout << name << " " << ort << " " << alter << std::endl;
}
};
void ausgabeFunktion(Student studentA){
std::cout << studentA.name << " " << studentA.ort << " " << studentA.alter << std::endl;
}
int main()
{
Student bernhard {"Cotta", 25, "Zillbach"};
bernhard.ausgabeMethode();
ausgabeFunktion(bernhard);
return 0;
}
@LIA.eval(["main.c"]
, g++ -Wall main.c -o a.out
, ./a.out
)
Methoden können auch von der Klassendefinition getrennt werden.
#include <iostream>
class Student{
public:
std::string name; // "-"
int alter;
std::string ort;
void ausgabeMethode(); // Deklaration der Methode
};
// Implementierung der Methode
void Student::ausgabeMethode(){
std::cout << name << " " << ort << " " << alter << std::endl;
}
int main()
{
Student bernhard {"Cotta", 25, "Zillbach"};
bernhard.ausgabeMethode();
return 0;
}
@LIA.eval(["main.c"]
, g++ -Wall main.c -o a.out
, ./a.out
)
Diesen Ansatz kann man weiter treiben und die Aufteilung auf einzelne Dateien realisieren.
Der Konzept der Modularisierung lässt sich unter C++ durch die Aufteilung der Klassen auf verschiedene Dateien umsetzen. Unser kleines Beispiel umfasst Klassen, die von einer main.cpp
aufgerufen werden.
#include <iostream>
#ifndef DATUM_H_INCLUDED
#define DATUM_H_INCLUDED
/* ^^ these are the include guards */
class Datum
{
public:
int tag;
int monat;
int jahr;
void ausgabeMethode(){
std::cout << tag << "." << monat <<"." << jahr <<std::endl;
}
};
#endif
#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED
#include <iostream>
#include <string>
#include "Datum.h"
class Student{
public:
std::string name; // "-"
Datum geburtsdatum;
std::string ort;
void ausgabeMethode(); // Deklaration der Methode
};
#endif
#include "Student.h"
void Student::ausgabeMethode(){
std::cout << name << " " << ort << " ";
geburtsdatum.ausgabeMethode();
std::cout << std::endl;
}
#include <iostream>
#include "Student.h"
int main()
{
Datum datum{1,1,2000};
Student bernhard {"Cotta", datum, "Zillbach"};
bernhard.ausgabeMethode();
return 0;
}
@LIA.eval(["Datum.h", "Student.h", "Student.cpp", "main.cpp"]
, g++ -Wall main.cpp Student.cpp -o a.out
, ./a.out
)
g++ -Wall main.cpp Student.cpp -o a.out
Definitionen der Klassen erfolgen in den Header-Dateien (.h), wobei für die meisten member-Funktionen nur die Deklarationen angegeben werden. In den Implementierungdateien (.cpp) werden die Klassendefinitionen mit include-Anweisungen bekannt gemacht und die noch nicht implementierten member-Funktionen implementiert.
Achtung: Die außerhalb der Klasse implementierte Funktionen erhalten einen Namen, der den Klassenname einschließt.
C++ verbietet für die Variablen und Objekte die gleichen Namen, erlaubt jedoch die variable Verwendung von Funktionen in Abhängigkeit von der Signatur der Funktion. Dieser Mechanismus heißt "Überladen von Funktionen" und ist sowohl an die global definierten Funktionen als auch an die Methoden der Klasse anwendbar.
**Merke: ** Der Rückgabedatentyp trägt nicht zur Unterscheidung der Methoden bei. Unterscheidet sich die Signatur nur in diesem Punkt, "meckert" der Compiler.
#include <iostream>
#include <fstream>
class Seminar{
public:
std::string name;
bool passed;
};
class Lecture{
public:
std::string name;
float mark;
};
class Student{
public:
std::string name; // "-"
int alter;
std::string ort;
void printCertificate(Seminar sem){
std::string comment = " nicht bestanden";
if (sem.passed)
comment = " bestanden!";
std::cout << "\n" << name << " hat das Seminar " << sem.name
<< comment;
}
void printCertificate(Lecture lect){
std::cout << "\n" << name << " hat in der Vorlesung " <<
lect.name << " die Note " << lect.mark << " erreicht";
}
};
int main()
{
Student bernhard {"Cotta", 25, "Zillbach"};
Seminar roboticSeminar {"Robotik-Seminar", false};
Lecture ProzProg {"Prozedurale Programmierung", 1.3};
bernhard.printCertificate(roboticSeminar);
bernhard.printCertificate(ProzProg);
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Achtung: Im Beispiel erfolgt die Ausgabe nicht auf die Console, sondern in ein ostream-Objekt, dessen Referenz an die print-Methoden als Parameter übergeben wird. Das ermöglicht eine flexible Ausgabe, z.B. in eine Datei, auf den Drucker etc.
#include <iostream>
#include <fstream>
class Seminar{
public:
std::string name;
bool passed;
};
class Lecture{
public:
std::string name;
float mark;
};
class Student{
public:
std::string name; // "-"
int alter;
std::string ort;
void printCertificate(std::ostream& os, Seminar sem);
void printCertificate(std::ostream& os, Lecture sem);
};
void Student::printCertificate(std::ostream& os, Seminar sem){
std::string comment = " nicht bestanden";
if (sem.passed)
comment = " bestanden!";
os << "\n" << name << " hat das Seminar " << sem.name << comment;
}
void Student::printCertificate(std::ostream& os, Lecture lect){
os << "\n" << name << " hat in der Vorlesung " << lect.name << " die Note " << lect.mark << " erreicht";
}
int main()
{
Student bernhard {"Cotta", 25, "Zillbach"};
Seminar roboticSeminar {"Robotik-Seminar", false};
Lecture ProzProg {"Prozedurale Programmierung", 1.3};
bernhard.printCertificate(std::cout, roboticSeminar);
bernhard.printCertificate(std::cout, ProzProg);
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Die Klasse spezifiziert unter anderem (!) welche Daten in den Instanzen/Objekten zusammenfasst werden. Wie aber erfolgt die Initialisierung? Bisher haben wir die Daten bei der Erzeugung der Instanz übergeben.
#include <iostream>
class Student{
public:
std::string name;
int alter;
std::string ort;
void ausgabeMethode(std::ostream& os); // Deklaration der Methode
};
// Implementierung der Methode
void Student::ausgabeMethode(std::ostream& os){
os << name << " " << ort << " " << alter << "\n";
}
int main()
{
Student bernhard {"Cotta", 25, "Zillbach"};
bernhard.ausgabeMethode(std::cout);
Student alexander { .name = "Humboldt" , .ort = "Berlin" };
alexander.ausgabeMethode(std::cout);
Student unbekannt;
unbekannt.ausgabeMethode(std::cout);
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Es entstehen 3 Instanzen der Klasse Student
, die sich im Variablennamen bernhard
, alexander
und unbekannt
und den Daten unterscheiden.
Im Grunde können wir unsere drei Datenfelder im Beispiel in vier Kombinationen initialisieren:
{name, alter, ort}
{name, alter}
{name}
{}
Elementinitialisierung beim Aufruf:
Umsetzung | Beispiel |
---|---|
vollständige Liste in absteigender Folge (uniforme Initialisierung) | Student Bernhard {"Cotta", 25, "Zillbach"}; |
unvollständige Liste (die fehlenden Werte werden durch Standard Defaultwerte ersetzt) | Student Bernhard {"Cotta", 25}; |
vollständig leere Liste, die zum Setzen von Defaultwerten führt | Student Bernhard {}; |
Aggregierende Initialisierung (C++20) | Student alexander = { .ort = "unknown"}; |
Wie können wir aber:
- erzwingen, dass eine bestimmte Membervariable in jedem Fall gesetzt wird (weil die Klasse sonst keinen Sinn macht)
- prüfen, ob die Werte einem bestimmten Muster entsprechen ("Die PLZ kann keine negativen Werte umfassen")
- automatische weitere Einträge setzen (einen Zeitstempel, der die Initialisierung festhält)
- ... ?
Konstruktoren dienen der Koordination der Initialisierung der Instanz einer Klasse. Sie werden entweder implizit über den Compiler erzeugt oder explizit durch den Programmierer angelegt.
class class_name {
access_specifier_1:
typ member1;
access_specifier_2:
typ member2;
memberfunktionA(...)
class_name (...) { // <- Konstruktor
// Initalisierungscode
}
};
class_name instance_name (...);
**Merke: ** Ein Konstruktor hat keinen Rückgabetyp!
Beim Aufruf Student bernhard {"Cotta", 25, "Zillbach"};
erzeugt der Compiler eine Methode Student::Student(std::string, int, std::string)
, die die Initialisierungsparameter entgegennimmt und diese der Reihenfolge nach an die Membervariablen übergibt. Sobald wir nur einen explizten Konstruktor integrieren, weist der Compiler diese Verantwortung von sich.
Entfernen Sie den Kommentar in Zeile 13 und der Compiler macht Sie darauf aufmerksam.
#include <iostream>
class Student{
public:
std::string name; // "-"
int alter;
std::string ort;
void ausgabeMethode(std::ostream& os){
os << name << " " << ort << " " << alter;
}
//Student();
};
// Implementierung der Methode
int main()
{
Student bernhard {"Cotta", 25, "Zillbach"};
bernhard.ausgabeMethode(std::cout);
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Dabei sind innerhalb des Konstruktors zwei Schreibweisen möglich:
//Initalisierung
Student(std::string name, int alter, std::string ort): name(name), alter(alter), ort(ort)
{
}
// Zuweisung innerhalb des Konstruktors
Student(std::string name, int alter, std::string ort):
this->name = name;
this->alter = alter;
this->ort = ort;
}
Die zuvor beschriebene Methodenüberladung kann auch auf die Konstruktoren angewandt werden. Entsprechend stehen dann eigene Aufrufmethoden für verschiedene Datenkonfigurationen zur Verfügung. In diesem Fall können wir auf drei verschiedenen Wegen Default-Werte setzen:
- ohne spezfische Vorgabe wird der Standardinitialisierungswert verwendt (Ganzzahlen 0, Gleitkomma 0.0, Strings "")
- die Vorgabe eines indivduellen Default-Wertes (vgl. Zeile 7)
#include <iostream>
class Student{
public:
std::string name;
int alter;
std::string ort; // = "Freiberg";
void ausgabeMethode(std::ostream& os){
os << name << " " << ort << " " << alter << "\n";
}
Student(std::string name, int alter, std::string ort): name(name), alter(alter), ort(ort)
{
}
Student(std::string name): name(name)
{
}
};
int main()
{
Student bernhard {"Cotta", 25, "Zillbach"};
bernhard.ausgabeMethode(std::cout);
Student alexander = Student("Humboldt");
alexander.ausgabeMethode(std::cout);
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Delegierende Konstruktoren rufen einen weiteren Konstruktor für die teilweise Initialisierung auf. Damit lassen sich Codeduplikationen, die sich aus der Kombination aller Parameter ergeben, minimieren.
Student(std::string n, int a, std::string o): name{n}, alter{a}, ort{o} { }
Student(std::string n) : Student (n, 18, "Freiberg") {};
Student(int a, std::string o): Student ("unknown", a, o) {};
#include <iostream>
class Student{
public:
std::string name;
int alter;
std::string ort;
Student(std::string n, int a, std::string o);
~Student();
};
Student::Student(std::string n, int a, std::string o): name{n}, alter{a}, ort{o} {}
Student::~Student(){
std::cout << "Destructing object of type 'Student' with name = '" << this->name << "'\n";
}
int main()
{
Student max {"Maier", 19, "Dresden"};
std::cout << "End...\n";
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
Destruktoren werden aufgerufen, wenn eines der folgenden Ereignisse eintritt:
- Das Programm verlässt den Gültigkeitsbereich (Scope, d.h. einen Bereich der mit
{...}
umschlossen ist) eines lokalen Objektes. - Ein Objekt, das
new
-erzeugt wurde, wird mithilfe vondelete
explizit aufgehoben (Speicherung auf dem Heap) - Ein Programm endet und es sind globale oder statische Objekte vorhanden.
- Der Destruktor wird unter Verwendung des vollqualifizierten Namens der Funktion explizit aufgerufen.
Einen Destruktor explizit aufzurufen, ist selten notwendig (oder gar eine gute Idee!).
**Aufgabe: ** Erweitern Sie das Beispiel um zusätzliche Funktionen, wie die Berechnung des Umfanges. Überwachen Sie die Eingaben der Höhe und der Breite. Sofern diese negativ sind, sollte die Eingabe zurückgewiesen werden.
#include <iostream>
using namespace std;
class Rectangle {
private:
int width, height;
public:
void set_values (int,int); // Deklaration
int area() {return width*height;} // Deklaration und Defintion
};
void Rectangle::set_values (int x, int y) {
width = x;
height = y;
}
int main () {
Rectangle rect;
rect.set_values (3,4);
cout << "area: " << rect.area();
return 0;
}
@LIA.eval(["main.cpp"]
, g++ -Wall main.cpp -o a.out
, ./a.out
)
-
Ansteuern einer mehrfarbigen LED
Die Auflistung der Memberfunktionen der entsprechenden Klasse finden Sie unter Link
-
Abfragen eines Sensors
Die Auflistung der Memberfunktionen der entsprechenden Klassen finden Sie unter Link
Der Beispielcode für die Anwendungen ist in den examples
Ordnern des Projektes enthalten.
Welches Schlüsselwort wird benutzt um eine Klasse zu definieren? [[class]]
{{1}}
Muss
[_____]
im unten aufgeführten Beispiel wirklich durch ein Semikolon ersetzt werden?
class class_name {
access_specifier_1:
member1;
access_specifier_2:
member2;
...
}[_____]
class_name instance_name;
[(X)] Ja [( )] Nein
{{2}}
Welche der folgenden Schlüsselwörter regeln die Zugriffsrechte bei Klassen und Klassen-Member? [[ ]] void [[X]] private [[ ]] general [[ ]] open [[X]] public [[X]] protected [[ ]] encrypted
{{3}}
Welcher Zugriffsbezeichner gilt standardmäßig für alle Member einer Klasse? [(X)] private [( )] protected [( )] public
{{4}}
Ersetzen Sie
[_____]
durch den Aufruf der MethodeBeispielauto
?
#include <iostream>
#include <string>
class Auto
{
public:
std::string Hersteller;
int Kilometerstand;
int PS;
void print(){
std::cout << Hersteller << " - " << PS <<" PS - " << Kilometerstand << " Kilometer" << std::endl;
}
};
int main()
{
Auto Beispielauto;
Beispielauto.Hersteller = "Hyundai";
Beispielauto.Kilometerstand = 49564;
Beispielauto.PS = 76;
[_____]
}
[[Beispielauto.print();]]
Wodurch muss
[_____]
ersetzt werden um den Kilometerstand vom ObjektBeispielauto
auf 40000 zu setzen?
#include <iostream>
#include <string>
class Auto
{
private:
std::string Hersteller;
int Kilometerstand;
int PS;
public:
void set_Hersteller(std::string _Hersteller){
Hersteller = _Hersteller;
}
void set_Kilometerstand(int _Kilometerstand){
Kilometerstand = _Kilometerstand;
}
void set_PS(int _PS){
PS = _PS;
}
};
int main()
{
Auto Beispielauto;
[_____]
}
[[Beispielauto.set_Kilometerstand(40000);]]
Vervollständigen Sie die Implementierung der Methode
ausgabeMethode
in dem Sie[_____]
durch noch fehlenden Teil ersetzen. Geben Sie die Antwort ohne Leerzeichen ein.
#include <iostream>
class Auto
{
public:
std::string Hersteller;
int Kilometerstand;
int PS;
void ausgabeMethode();
};
// Implementierung der Methode
void [_____]{
std::cout << Hersteller << " - " << Kilometerstand << " km " << PS << " PS" << std::endl;
}
int main()
{
Auto beispielauto {"Tesla", 25000, 283};
beispielauto.ausgabeMethode();
return 0;
}
[[Auto::ausgabeMethode()]]
Im Programm
main.cpp
soll die in der DateiAuto.h
deklarierte KlasseAuto
verwendet werden. Welche Dateien werden dafür benötigt? [[ ]]main.h
[[X]]main.cpp
[[X]]Auto.h
[[X]]Auto.cpp
{{1}}
Im folgenden Programm soll die in der Datei
Auto.h
deklarierte KlasseAuto
verwendet werden. Wodurch muss[_____]
ersetzt werden um das zu ermöglichen? Es kann davon ausgegangen werden, dass alle benötigten Dateien im selben Ordner liegen.
#include <iostream>
#include [_____]
int main()
{
Auto Beispielauto{"Tesla", 25000, 283};
Beispielauto.ausgabeMethode();
return 0;
}
[["Auto.h"]]
Wie lautet die Ausgabe dieses Programms?
#include <iostream>
class Force
{
public:
double Newton(double mass){
double F = mass * 9.81;
return F;
};
double Newton(double mass, double acc){
double F = mass * acc;
return F;
};
};
int main()
{
Force Kraft;
double G = Kraft.Newton(10);
double F = Kraft.Newton(10, 10);
double R = G + F;
std::cout << R << std::endl;
}
[[198.1]]
Wie lautet die Ausgabe dieses Programms?
#include <iostream>
class Student{
public:
std::string name;
int alter;
std::string lieblingstier = "Dikdik";
void ausgabeMethode(std::ostream& os){
os << name << " " << lieblingstier << " " << alter;
}
Student(std::string name, int alter, std::string lieblingstier): name(name), alter(alter), lieblingstier(lieblingstier)
{
}
Student(std::string name): name(name),alter(0)
{
}
};
int main()
{
Student alexander = Student("Humboldt");
alexander.alter = 19;
alexander.ausgabeMethode(std::cout);
return 0;
}
[[Humboldt Dikdik 19]]
{{1}}
Wie lautet die Ausgabe dieses Programms?
#include <iostream>
class Student{
public:
std::string name;
int alter;
std::string lieblingstier = "Dikdik";
void ausgabeMethode(std::ostream& os){
os << name << " " << lieblingstier << " " << alter;
}
Student(std::string name, int alter, std::string lieblingstier): name(name), alter(alter), lieblingstier(lieblingstier)
{
}
Student(std::string name): name(name),alter(0)
{
}
};
int main()
{
Student alexander = Student("Humboldt", 23, "Einhorn");
alexander.ausgabeMethode(std::cout);
return 0;
}
[[Humboldt Einhorn 23]]
Wie lautet die Ausgabe dieses Programms?
#include <iostream>
#include <string>
class Auto
{
public:
std::string Hersteller;
int Kilometerstand;
int PS;
~Auto(){
std::cout << "!";
}
};
int main()
{
Auto Beispielauto{"Hyundai", 25000, 76};
std::cout << Beispielauto.Hersteller << " ";
std::cout << Beispielauto.Kilometerstand << " " << Beispielauto.PS;
}
[[Hyundai 25000 76!]]