struct-TypenWas tun?
- Die Lösung sind Zeiger!
#include <stdint.h>
#include <stdio.h>
struct Person {
uint32_t geb_jahr;
uint32_t geb_monat;
uint32_t geb_tag;
char name[64];
};
int main() {
struct Person alice;
printf("%lu\n", sizeof(alice));
return 0;
}
âHugo Hase gibt euch seine Visitenkarteâ
Zeiger referenziert Ort im Hauptspeicher
Wie eine Seitenangabe im Inhaltsverzeichnis
Eine Zeile mit Seitenverweis anstatt zig Seiten an Inhalt
Wichtig: Zeiger in C sind ebenfalls Variablen
Notation: int * ptr; (Zeiger auf einen int)
GröĂe eines Zeigers ist konstant, aber abhĂ€ngig von Rechnerarchitektur
SpeichergröĂe
Adresse, auf die gezeigt wird, kann an beliebiger Stelle stehen
Bei Funktionen
#include <stdio.h>
int main() {
int i = 1;
int * ptr = &i; // Stack-Adresse von i
printf("int: %p\nptr: %p\n", &i, ptr);
return 0;
}
Zugriff auf den Wert ist ebenso einfach â Dereferenzierungsoperator *
Verwechselungsgefahr: Multiplikationsoperator oder Pointer-Stern
Beliebter Fehler: Dereferenzieren wurde vergessen â Adresse anstatt Wert
Wichtig: Typ einer Zeigervariable bestimmt die Anzahl der gelesenen Bytes ab der Adresse des Zeigers
#include <stdio.h>
int main() {
int i = 1;
int * ptr = &i;
*ptr = 3;
printf("int: %d\nptr: %d\n", i, *ptr);
return 0;
}
Alt: Wertetausch mit Kopien
(By Value)
#include <stdio.h>
void swap_values(int one, int two) {
int temp;
temp = one;
one = two;
two = temp;
}
int main() {
int a = 1, b = 2;
swap_values(a, b); // Geht nicht
printf("a: %d, b: %d\n", a, b);
return 0;
}
Neu: Wertetausch mit Zeigern
(By Reference)
#include <stdio.h>
void swap_values(int* one, int* two) {
int temp;
temp = *one;
*one = *two;
*two = temp;
}
int main() {
int a = 1, b = 2;
swap_values(&a, &b);
printf("a: %d, b: %d\n", a, b);
return 0;
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuninitialized"
#include <stdio.h>
void swap_values(int * one, int * two) {
int temp;
temp = *one;
*one = *two;
*two = temp;
}
int main() {
int a = 1, b = 2;
int *pointer_a = &a;
int *pointer_b; // Nicht initialisiert
swap_values(pointer_a, pointer_b);
printf("a: %d, b: %d\n", a, b);
return 0;
}
Welchen Wert hat dieser Zeiger nach der Deklaration? (64-Bit-Architektur)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuninitialized"
#include <stdio.h>
int main() {
int *ptr;
printf("Adress: %p", ptr);
return 0;
}
0x0
Gilt per Konvention als âsichererâ
â Standardwert fĂŒr einen nicht initialisierten Zeiger
Zugriff auf Adresse 0x0 immer noch möglich
â Absturz bei Zugriff
Sir Tony Hoare (Informatiker)
Bekannt fĂŒr: Quicksort, Hoare-KalkĂŒl, Nullreferenz
âI call it my billion-dollar mistake. [âŠ] I couldnât resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes [âŠ]â
Quelle: Vortrag InfoQ.com (2009)
Zeiger können NULL/nullptr sein
NULL hĂ€ufig als #define NULL 0 implementiert (suboptimal)nullptr seit C++11 bzw. C23 (spezielle Bedeutung)nullptr ist immer zu bevorzugen, sofern verfĂŒgbarGuter Ton: Zeiger vor Verwendung ĂŒberprĂŒfen
â Defensives Programmieren
Was aber, wenn der Zeiger nicht initialisiert ist?
Verhalten einer Funktion dokumentieren, einschlieĂlich erwarteter Parameterwerte
â Verantwortung auf die Benutzer:in abwĂ€lzen
Externe Regeln erzwingen
-WunitializedAber wie sieht der Speicher aus?
SP) zeigt stets auf oberstes ElementStack (.stack) ist nur ein Teil des Speichers eines Prozesses
Weitere wichtige Speichersegmente sind
.heap, .bss, .text und .data
.rodata â enthĂ€lt StringsIn der Praxis: Beginnt bei gröĂter Adresse
(â Base Adress)
âStack wĂ€chst nach untenâ
Neue Variablen werden unterhalb der Startadresse angelegt
Randbemerkung: Abstand/Differenz zu einer Startadresse â Offset
Grenzt typischerweise direkt an
.data, .bss und .text
Speicherort fĂŒr dynamisch allozierte Objekte
â malloc()
Stack und Heap werden bei Programmstart erzeugt
.data und .bss sind Teil des
Datensegments (globale und statische Variablen)
.data enthÀlt initialisierte Variablen
static int i = 1;.bss enthÀlt nicht-initialisierte Variablen
static int j;0 initialisiert.bss = Block Started By Symbol
.text beinhaltet Programmcode (â Maschinencode)
SchreibgeschĂŒtzt, nur Lesen ist erlaubt
(im Gegensatz zu .data und .bss)
Grund: Programm darf sich nicht selber modifizieren
Bei Programmstart: BS kopiert .text, .bss und .data aus Programmdatei in Speicher
âĂberlaufenâ von Stapel und Halde einfach möglich
FĂŒr Heap: Nicht ohne weiteres erkennbar
(meist mit externen Werkzeugen)
FĂŒr Stack: Bekannte Werte auf Stack schreiben
(â Canaries)
#include <stdio.h>
int main(int argc, char* argv[]) {
char buf[4];
buf[0] = 'H';
buf[1] = 'e';
buf[2] = 'l';
buf[3] = 'l';
buf[4] = 'o';
buf[5] = '!';
buf[6] = '\0';
printf("%s\n", buf);
return 0;
}
errnomalloc() signalisiert Fehler ĂŒber RĂŒckgabewertmalloc() NULL zurĂŒckerrnoerrno schauen wir uns jetzt genauer anerrnoerrno.h zu findenerrno gesetzterrno == EEXIST; â âFile existsâstrerror(errno);ENOMEM (steht fĂŒr Error: No Memory)errno#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
const char * file_path = "example.txt";
// Ăffne Datei zum Lesen
FILE * f_handle = fopen(file_path, "r");
if (f_handle == NULL) {
printf("Fehler: %s\n", strerror(errno));
} else {
fclose(f_handle); // AufrÀumen
}
return 0;
}
errnoProgrammierer:in kann errno ignorieren
Aber: Einzige Möglichkeit, Fehler zu detektieren
Beispiel: malloc() ohne Fehlerbehandlung
Verwendung von errno wird ausfĂŒhrlich dokumentiert
â siehe Dokumentation â Lest sie! đ
Programmiert bitte defensiv â behandelt möglichst viele FehlerfĂ€lle
i3 = (int ***) &i; // Prinzipiell erlaubt (aber meist sinnfrei)Zeiger mit mehrfacher Indirektion erfordern viel Aufmerksamkeit
â sehr groĂes Fehlerpotential
Tipp: Bei Zuweisungen/Zugriffen mentale Checkliste durchgehen
âWelche Typen haben die Variablen?â
int **i, *j, k = 1;âPassen die Typen schon, wenn ich die so verwende?
Oder muss ich noch passend referenzieren/dereferenzieren?â
j = &k; i = &j;âWelchen Wert brauche ich gerade? Was muss ich dafĂŒr tun?â
int k*(*i);Zeiger werden auch in Arrays verwendet
Genauer gesagt: Arrays sind Pointer mit zusÀtzlicher Dereferenzierung und LÀngeninformation
Beispiel: char arr[4];
Arraybezeichner arr ist Zeiger auf den Beginn des Speicherblocks
(\(\hat{=}\) erster Eintrag des Arrays arr[0])
Arrayindex gibt Offset von der Startadresse an
Bei Umwandlung von Array zu Pointer entsteht Informationsverlust (LĂ€nge)
â Wird auch Pointer Decay genannt (Herabstufung zu Zeiger)
Offset-VerÀnderung um \(n\) entspricht Byte-VerÀnderung von
\(n\) x sizeof(Datentyp)
#include <stdio.h>
int main() {
double d = 1.0;
double * ptr = &d;
printf("%p\n", ptr);
ptr += 1; // ptr + 8 Bytes
printf("%p\n", ptr);
char * c = (char*)"c";
printf("%ld\n", (c+1) - c);
}
#include <stdio.h>
int main() {
int * ptr;
int array[4] = {1, 2, 3, 4};
ptr = &array[0]; // array[0]
ptr += 1; // ptr == &array[1]
++ptr; // ptr == &array[2]
ptr--; // ptr == &array[1]
printf("%d\n", *ptr);
ptr += 10; // GEFAHR
return 0;
}
#include <stdio.h>
int main() {
int arr[4] = {1, 2, 3, 4};
int *ptr = arr;
int i = arr[2]; // Standard, bereits bekannt
int j = *(arr + 2); // Umwandlung des Compilers von arr[2]
int k = *(&(arr[0]) + 2);
int l = ptr[2]; // Arraynotation geht auch mit Zeigern
int m = 2[arr]; // *(2+arr) == *(arr+2)
printf("i: %d\nj: %d\nk: %d\nl: %d\nm: %d\n", i, j, k, l, m);
return 0;
}
đšAchtung: Zeigerarithmetik wird nicht ĂŒberprĂŒft
â Berechnung von beliebigen Adressen möglich
â Bei Dereferenzierung wird String ab 0x24 als int interpretiert
Pointer-Arithmetik nur sehr behutsam und sparsam benutzen
struct).) wird Pfeiloperator (->) genutzt#include <stdio.h>
#include <stdlib.h>
struct A { int i; struct A * ptr; };
int main() {
struct A a = { 1, nullptr };
struct A *b = (struct A *) malloc(sizeof(struct A));
b->i = 2;
b->ptr = nullptr;
a.ptr = b;
printf("A: %d\nB: %d\n", a.i, a.ptr->i); // Pfeil -> nur fĂŒr Zeiger
free(a.ptr);
printf("A: %d\nB: %d\n", a.i, a.ptr->i); // UB: Use after free()
}
Pfeiloperator nur auf Zeigervariablen anwenden (beliebter Fehler)
#include <stdio.h>
struct Node {
int i;
struct Node * next;
};
void add_node(struct Node * root, struct Node * node) {
if (root == nullptr) { return; }
struct Node * current = root;
while (current->next != nullptr) {
current = current->next;
}
current->next = node;
}
int main() {
struct Node n = { 1, nullptr };
struct Node m = { 2 }; // m.next mit 0 initialisiert
add_node(&n, &m);
printf("%d\n", n.next->i);
}
#include <stdio.h>
struct Example {
char foo[3]; // 3 Bytes
int bar; // 4 Bytes
bool baz; // 1 Byte
};
int main() {
struct Example ex = { "ab", 4, true };
printf("Size: %ld, Alignment: %ld\n", sizeof(ex), alignof(struct Example));
}
Speicher ist typischerweise an festen Grenzen ausgerichtet
Einzelne Elemente kleiner als Alignment-Wert
â EinfĂŒgen von âPolsterungâ (Padding Bytes)
Alignment-Raster abfragen mit alignof()
Zwei Möglichkeiten, Padding zu eliminieren
(â spart Speicherplatz)
__attribute__((packed))
BeintrĂ€chtigt ggf. Performance (â â„2 Zugriffe notwendig)
Nur bei bestimmten Szenarien sinnvoll
(Beschreiben einer Speicherzelle)
#include <stdio.h>
struct Example {
char foo[3];
bool baz; // Vertauscht mit bar
int bar;
};
int main() {
struct Example ex = { "ab",
true, 4 };
printf("Size: %ld", sizeof(ex));
}
union, aber viel gefĂ€hrlicherđš#include <limits.h>
#include <stdio.h>
struct A { int i; };
struct B { double d; struct A* a; };
int main() {
struct A a = { 127 };
struct B* b = (struct B*)&(a);
printf("A: %d\n", a.i);
printf("B: %f\n", b->d);
}
#include <stdio.h>
void print_float_bits(float f) {
int * i = (int *) &f;
for (unsigned int index = 0; index < sizeof(f) * 8; ++index) {
int bit = ((1 << index) & *i) > 0;
printf("%c", bit == 1 ? '1' : '0');
}
}
int main() {
print_float_bits(3.5f);
return 0;
}
Das letzte Beispiel zeigt: C hat Probleme!
Einerseits: Typangabe bei Deklaration immer erforderlich
Andererseits: Umgehung des Typsysstems sehr einfach möglich
Wird hĂ€ufig âstark typisiert, aber mit schwacher Umsetzungâ bezeichnet
(strongly typed, weakly enforced)
C++ ist hier deutlich strenger, aber deswegen auch komplizierter
#include <stdio.h>
int add (int x, int y) { return x + y; };
int sub (int x, int y) { return x - y; };
int bigger_num (int x, int y) { return (x > y) ? x : y; }
int call_func(int (*func)(int, int), int int1, int int2) {
return func(int1, int2);
}
int main() {
printf("%d\n", call_func(add, 1, 2));
printf("%d\n", call_func(sub, 1, 2));
printf("%d\n", call_func(bigger_num, 1, 2));
}
volatile-O1 bis -O3volatile: volatile int * j;constvolatile gibt es SchlĂŒsselwort constconst kann einmal* initialisiert werden, danach unverĂ€nderbar"Hello World"; // Typ: const char *volatile und constconst etwas Vorsicht gebotenđšconst definierte Typen
const int * i;int * const j;const int * const k;int j = 42;int *k = 0x4711;selbst Variablen, deren GröĂe sich nach der Architektur richtet
und nicht nach dem Datentyp, auf den sie zeigen
können beliebige Adressen enthalten und greifen dort auf Speicher zu đš
verÀnderbar: Zeigerarithmetik
k = k + 12;