15 KiB
Workbook
- Workbook für das Praktikum Algorithmen und Programmierung 1
- Fachfragenkatalog
- Was ist der Unterschied zwischen
Float
undDouble
? - Wofür werden
structs
verwendet, und welche Bestandteile werden benötigt? - Was ist der Unterschied zwischen Syntax und Semantik?
- Was versteht man unter dem
fall-through
? Wie ist diese verhalten zu verhindern, wann könnte es nützlich sein? - Erklären Sie den konditionalen (ternären) Operator Anhand eines Beispieles.
- Was ist der Unterschied zwischen Pre- und Postinkrementation und wie werden sie notiert?
- Was ist der Unterschied zwischen einer
while
und einerdo-while
Schleife? - Welche Probleme können bei einer rekursiven Implementation einer Funktion auftreten?
- Was ist der Unterschied zwischen einer Call-by-Reference und einer Call-by-Value Funktion? Nennen Sie für beide Arten Beispielfunktionen aus der C-Standardbibliothek.
- Aus welchem Grund sollte die
goto
-Anweisung bei der Programmierung grundsätzlich vermieden werden? - Wodurch wird ein Character-Array zu einem String?
- Welche Informationen befinden sich in einer Deklarationsdatei (Header)?
- Preprocessor Erklärung
- Was ist der Unterschied zwischen
- Fachfragenkatalog
Workbook für das Praktikum Algorithmen und Programmierung 1
Fachfragenkatalog
Was ist der Unterschied zwischen Float
und Double
?
Float
und Double
werden beide genutzt, um Fließkommazahlen zu speichern. Der Unterschied liegt darin, dass Double
, wie der Name schon sagt, doppelte Genauikeit hat. Bei modernen Systemen hat ein float
üblicherweise 32 bit, ein Double 64
bit.
Wofür werden structs
verwendet, und welche Bestandteile werden benötigt?
Structs
werden verwendet, um zusammenhängende Daten an einem Ort zu speichern. Genauer gesagt wird der Code im Computer dadurch in direkt nebeneinander liegenden Teilen des Arbeitsspeichers gespeichert. Structs bestehen immer aus dem Keyword struct
, gefolgt von den Namen, einer geöffneten geschweiften Klammer {
, Inhalten des struct, getrennt durch Kommata, einer schließenden geschweiften Klammer }
, abgeschlossen mit einem Semikolon. Struct selbst können, anders als Klassen, keine Methoden oder Funktionen enthalten, wohl aber function pointers. Hier ein Beispiel:
#include <stdbool.h> // Für den bool typedef, sowie true und false
struct Person {
char* name; // Hier ein char Pointer, um strings darzustellen, da die größe zum Zeitpunkt der Definition noch nicht bekannt ist.
int alter; // Ganze Zahl, die das Alter der Person repräsentiert
bool lebendig; // Bool, der angibt, ob die Person lebt
void (*lebe)(); // pointer zu einer lebe funktion
};
void lebe_Person(struct Person* person) { // Implementation der Lebe Funktion aus dem Person struct.
if (!person->lebendig) { // Führe den Code nur aus, wenn die Person nicht lebt
person->lebendig = true; // Macht die Person lebendig
}
}
Oft wird auch ein typedef struct StructName StructName
verwendet, damit man nicht immer struct davor schreiben muss.
Was ist der Unterschied zwischen Syntax und Semantik?
Syntax bezeichnet die korrekte Formulierung von Code auf einer rein Formellen Basis. Sind alle Semikolons da, wo sie sein sollen? Sind alle (geschweiften) Klammern richtig gesetzt? Wurden korrekte Typen angegeben? Diese Dinge werden überprüft während das Programm kompiliert wird. Bei Fehlern wird der Compiler die Kompilation beenden und einen Fehler ausgeben. Semantik hingegen bezeichnet die Logik, die das Programm ausführt. Werden die richtigen Konditionen überprüft? Werden Variablen oder Konstanten die richtigen Werte zugewiesen? Werden die richtigen Funktionen aufgerufen? Beispiel für ein Programm mit einem Syntaxfehlern:
#include <stdio.h>
void main() { //Deklaration von main als void funktion ist falsch
printf("Hello Word!\n") // Fehldes Semikolon
return 0; // Void Funktion gibt einen Wert aus.
}
Beispiel für ein Programm mit einem Semantikfehler:
#include <ctype.h> // Inkludiert für isdigit()
#include <stdbool.h> // Inkludiert für den Bool typedef
#include <stdio.h> // Für printf
bool is_int(char* str) { // Funktion, die überprüft, ob ein gegebener String eine ganze Zahl ist oder nicht
if (str == NULL) {return false;} // Funktion gibt falsch zurück, wenn der gegebene String NULL ist. Sicherheitsmaßnahme um zu verhindern, dass ein NULL pointer dereferenziert wird.
while (str != '\0') { // Code wird ausgeführt, bis der NULL Terminator erreicht wird
if (!isdigit(str) && !(str == '0')) { // Überprüft, ob der aktuelle char entweder nicht 1-9 oder nicht 0 ist.
return true; // Die Funktion gibt hier true zurück, obwohl ein Zeichen außer 0-9 vorkommt.
}
str++; // Geht zum nächsten Character in str.
}
return false; // Gibt false zurück, obwohl es eigentlich ein Integer ist.
}
// Zum verdeutlichen:
int main() {
char[10] num = "0123456789"; // Definitiv eine ganze Zahl
if (is_int(&num)) {
printf("Es ist eine Zahl.\n");
} else {
printf("Es ist keine Zahl.\n");
}
return 0;
}
Dieser Code würde Es ist keine Zahl.
ausgeben, obwohl es definitiv eine Zahl ist. Die meisten bugs werden durch Semantikfehler verursacht, selten sind diese auch in der Standardbibliothek vorhanden. Durch Semantikfehler entstehen auch größere Probleme, wie z. B. NULL-ptr dereference, free-after-use etc.
Was versteht man unter dem fall-through
? Wie ist diese verhalten zu verhindern, wann könnte es nützlich sein?
fall-through
beschreibt ein Verhalten bei switch
statements, wenn nach einem case
Block kein break;
kommt und dadurch der nächste Fall bis zu einem break
ausgeführt wird. Als Beispiel:
#include <stdio.h>
int main() {
int wert = 1;
switch (wert) {
case 1:
printf("Der Wert ist 1.\n"); // Kein break; in diesem Block
case 2:
printf("Der Wert ist 2.\n");
break;
case 3:
printf("Der Wert ist 3.\n");
break;
default:
printf("Der Wert ist weder 1 noch 2 noch 3.\n"); // Hier braucht es kein break; weil das der letzte Block ist.
}
return 0;
}
Hier tritt ein fall-through
auf, weil in dem Block von case 1
kein break;
folgt. Verhindert werden kann dieses Verhalten, indem nach jedem Block ein break;
geschrieben wird, wodurch das switch
statement sofort beendet wird.
Ein fall-through
kann aber auch nützlich sein, z. B. wenn man möchte, dass Code ab einem bestimmten Punkt ausgefürt wird. Ein Beispiel:
#include <stdio.h>
int berechne_semester_bis_studienende(int bestandene_semester) { // Hier wird die Regelstudienzeit angenommen
switch (bestandene_semester) {
case 0:
bestande_semester++;
case 1:
bestande_semester++;
case 2:
bestande_semester++;
case 3:
bestande_semester++;
case 4:
bestande_semester++;
case 5:
bestande_semester++;
}
return bestande_semester; // Eigentlich könnte man diese ganze Funktion mit 6-bestandene_semester ersetzen
}
int main() {
int buffer;
printf("Gib ein, wie viele Fachsemester du bereits bestanden hast: ");
scanf("%d", &buffer);
printf("Dein Studium wäre in Regelstudienzeit in %d Monaten abgeschlossen.\n", berechne_semester_bis_studienende(buffer));
return 0;
}
Man könnte diesen Code zwar grundsätzlich anders einfacher formulieren, aber das ist ein Beispiel, wo switch
und fall-through
als Einstiegspunkt genutzt werden.
Oft sind fall-throughs
unabsichtlich. Ein Entwickler hat ein break;
vergessen, weshalb sie in solchen Fällen Semantikfehler sind.
Dies ist aber nicht immer der Fall. Fall-throughs
sind eine durchaus nützliche Programmierpraxis, wenn der geswitchte Wert als Einstiegspunkt verstanden wird und damit in eine Menge von immer gleich bleibende Operationen auch erst an einem späteren Punkt eingestiegen werden kann, zum Beispiel wenn bestimmte Bedingungen bereits erfüllt sind.
Erklären Sie den konditionalen (ternären) Operator Anhand eines Beispieles.
Der ternäre Operator folgt der Syntax Bedingung ? ausdruck_wenn_war : ausdruck_wenn_falsch;
. Hier ein Beispiel, wo zugewiesen wird, ob jemand Erwachsen oder Minderjährig ist, abhängig vom Alter.
#include <stdio.h>
int main() {
int buffer;
printf("Gib dein Alter ein. ");
scanf("%d", &buffer);
char *person = ((buffer < 18) ? "Minderjähriger" : "Erwachsener"); // Klammern helfen bei der Leserlichkeit. String literals sind eigentlich nur const char pointers.
printf("Du bist ein %s.\n", person);
return 0;
}
In diesem Beispiel gibt der Nutzer ein numerisches Alter ein und Mithilfe des ternären Operators wird dem Nutzer entweder Erwachsener oder Minderjähriger zugewisen. Minderjähriger falls Alter < 18 ist, in allen anderen Fällen Erwachsener.
Was ist der Unterschied zwischen Pre- und Postinkrementation und wie werden sie notiert?
Beide nutzen den Inkrementationsoperator, das ++
. Bei der Preinkrementation wird Die Variable sofort inkrementiert, und der alte Wert kann nicht noch anderswo zwischengespeichert werden. Pre, also vor, steht dabei für vor dem zuweisen. Sie wird mit ++a
notiert, wenn a die Variable ist, die inkrementiert werden soll. Bei der Postinkrementation, post steht hier für nach, wird die Variable inkrementiert, nachdem der Wert einer anderen Variable zugewiesen wurde, wenn man das machen möchte. Die Notation hierfür ist a++
, wenn a die Variable ist, die inkrementiert werden soll. Zur Verdeutlichung kann dieser Code helfen:
#include <stdio.h>
int main() {
int a = 5;
int b = 5;
int c = a++;
int d = ++b;
printf("A: %d\n", a);
printf("B: %d\n", b);
printf("C: %d\n", c);
printf("D: %d\n", d);
return 0;
}
Die Ausgabe hiervon ist:
A: 6
B: 6
C: 5
D: 6
Daran kann man das Verhalten gut erkennen.
Was ist der Unterschied zwischen einer while
und einer do-while
Schleife?
Beide Schleifen führen Bedingungen aus, solange eine bestimmte Bedingung erfüllt ist. Der große Unterschied dabei ist, dass bei der while-Schleife, der Code unter Umständen gar nicht ausgeführt wird, wenn die Eingangsbedingung nicht wahr ist. Bei der do-while Schleife wird der Code mindestens einmal ausgeführt. Hier beide Schleifen in einem Code Beispiel:
#include <stdio.h>
int main() {
while (false) { // Wird niemals ausgeführt.
printf("Ausgabe aus der while-Schleife.\n");
}
do { // Wird ausgeführt
printf("Ausgabe aus der do-while Schleife.\n");
} while (false); // Ab hier wird nicht mehr ausgeführt
return 0;
}
Welche Probleme können bei einer rekursiven Implementation einer Funktion auftreten?
Rekursive Implementation können verschiedene Probleme aufweisen. Ein häufiges Problem ist eine extrem lange Laufzeit (große Zeitkomplexität). in gutes Beispiel hierfür ist eine rekursive Berechnung einer Zahl aus der Fibonacci Reihe (ohne caching). Hierbei werden häufig immer wieder die gleichen Zahlen als Zwischenschritt berechnet, weshalb die Laufzeit für die Berechnung vom 100sten Element viele Jahre dauert. Dadurch sind rekursive Ansätze für große Berechnungen meist aus Performance gründen nicht sinnvoll. Ein anderes Problem wäre unendliche Laufzeit, wenn ein bestimmte Bedingung gar nicht erfüllt werden kann, in welchem Fall sich die Funktion selbst immer wieder aufruft und dadurch nie endent. So etwas kann in der Implementation verhindert werden.
Was ist der Unterschied zwischen einer Call-by-Reference und einer Call-by-Value Funktion? Nennen Sie für beide Arten Beispielfunktionen aus der C-Standardbibliothek.
Bei einer Call-by-Value funktion, wird direkt ein Wert in die Funktion gegeben. Dieser wird dann kopiert und alle Modifikationen dieses Wertes passieren in der Kopie, statt in dem Wert (z. B. einer Variable), die der Funktion direkt gegeben wurde. Eine Call-By-Reference Funktion bekommt stattdesssen einen pointer zu einer Variable. Dadurch das statt einem Wert eine Speicheradresse gegeben wird, wird der Wert der Variable direkt modifiziert. Dementsprechend sind call-by-reference Funktionen genau dann sinnvoll, wenn der Wert der Variable in der Funktion selbst modifiziert werden soll.
Ein Besispiel für eine Call-By-Reference Funktion wäre zum Beispiel scanf()
auf stdio.h
, welche eine Speicheradresse zu dem Buffer bekommt, in den geschrieben werden soll.
Ein Beispiel für eine Call-By-Value Funktion ist z. B. strcmp()
aus string.h
, in der zwei Strings verglichen werden. Da die strings hier selbst nicht bearbeitet werden müssen, sondern nur die Werte verglichen werden, ist dieser Ansatz hier sinnvoller.
Aus welchem Grund sollte die goto
-Anweisung bei der Programmierung grundsätzlich vermieden werden?
Code, der über goto
strukturiert ist, neigt dazu sehr unleserlich, unübersichtlich und schwer verständlich zu werden. Es gibt allerdings Ausnahmefälle, in denen goto
die Leserlichkeit verbessern kann, diese sind jedoch selten. goto
Kann außerdem dazu genutzt werden, um die Struktur des C codes ähnlicher zu der von Assembly code zu machen, was in seltenen Niechenfällen nützlich sein kann.
Wodurch wird ein Character-Array zu einem String?
Strings sind char
arrays, die mit einem Besondern byte, also char
, dem sogenannten Null-Terminator ('\0'
), beendet werden. Dadurch können Funktionen, die mit den strings arbeiten, erkennen, das a) der gegebene Array tatsächlich ein String ist und b) der String am Ende tatsächlich beendet ist.
Welche Informationen befinden sich in einer Deklarationsdatei (Header)?
Ein Header enthält hauptsächlich leere Implementation von Funktionen (Nur die Kopfzeile wo Name, typ und Parameter spezifiziert werden), structs
, aber keine Implementationen von structs, d. H. in dem struct werden keine Werte zugeordnet, und außerdem Deklarationen von globalen Variablen (aber nur namentlich, ohne Wertzuweisung), Makros und typedefs. Zudem findet in headern oft das inkludieren von Bibliotheken statt, die dann in dem Progran genutzt werden. Die Funktionen werden dann in Dateien mit dem gleichen Namen (Mit der Dateiendung .c) implementiert. Diese Datei inkludiert den Header. Ferner gibt es in header Dateien oft sogenannte include guards, welche dazu dienen, redundaten imports zu vermeiden. Ein Beispiel für eine Header Datei wäre zum Beispiel:
// Code aus library.h
#ifndef LIBRARY_H_ // Überprüft, ob LIBRARY_H_ definiert ist. Wenn nicht geht es weiter, wenn doch wird der include direkt beendet.
#define LIBRARY_H_ // Macro als Teil des Include guards definiert
#include <stdio.h> // Importiert stdio.h, damit es später benutzt werden kann.
#include <stdlib.h>
#define ARR_SIZE 1024 // "Magische Zahl", wird hier benutzt um eine konstante größe von Arrays festzulegen.
typedef unsigned char uchar; // Definiert den typ uchar, der gleichbedeutend mit unsigned char ist. Nützlich für 8-bit Zahlen von 0-8
struct Tier {
int alter;
char spezies[ARR_SIZE];
uchar sprunghöhe;
};
struct Tier* erschaffe_tier(int alter, char* spezies, uchar sprunghöhe);
void springe (struct Tier tier);
int Tieranzahl;
#endif // LIBRARY_H_
Das hier wäre die Header Datei. Sie könnte zum Beispiel so benutzt werden:
// Code in library.c
#include "library.h"
struct Tier* erschaffe_tier(int alter, char* spezies, uchar sprunghöhe) {
struct Tier* neues_tier = (struct Tier*) malloc(sizeof(struct tier));
neues_tier->alter = alter;
neues_tier->spezies = spezies;
neues_tier->sprunghöhe = sprunghöhe;
tieranzahl++;
return neues_tier;
}
void springe(struct Tier tier) {
printf("Das tier ist %d meter hoch gesprungen.", tier.sprunghöhe);
}
Das könnte dann in einer anderen Datei benutzt werden:
// code in main.c
#include "library.h"
int main(){
struct Tier* graues_riesenkänguru = erschaffe_tier(5, "Känguru", 3);
springe(*graues_riesenkänguru);
printf("Das tier ist %d Jahre alt.\n", graues_riesenkänguru->alter);
printf("Das tier hat die Spezies %s\n", graues_riesenkänguru->spezies);
printf("Insgesamt wurden %d Tiere erschaffen", tieranzahl);
return 0;
}
Preprocessor Erklärung
Der Präprozessor in C ist ein Teil des Kompilierungsvorgangs