Falscher short Specifier löscht Inhalt von char Array

theqwe

Aktives Mitglied
Hallo zusammen,

nach einer Menge Java wollte ich mich mal mit der Sprache C auseinander setzen. Ich fange auch mit den Basics an und zermürbe mir gerade bei einer Sache den Kopf.
Ich habe ein char array für den Namen des Benutzers und ein unsigned short für das Alter. Wenn ich beides Abfrage und mit printf ausgebe, wird der Name aber gelöscht und nur durch ein Leerzeichen angezeigt.

C:
    char name[20];
    unsigned short age;

    printf("Hi there\n");
    printf("What's your name: ");   
    scanf("%s", &name);
    printf("And your age: ");
    scanf("%u", &age);

    printf("Welcome %s to the first C program.\n", name);
    printf("I see you're %u years old.\n", age);
    return 0;

Nach ein bisschen Recherche habe ich rausgefunden, dass bei Zeile 8 und 11 ein %hu hin muss. Danach klappt alles. Setze ich in Zeile 2 ein unsigned short* age; klappt es auch mit %u. Jetzt frage ich mich nur, warum der Name bei %u verschwindet und warum es bei unsigned short* auch mit %u geht. Wo ist mein Denkfehler?
 

theqwe

Aktives Mitglied
Ich bin jetzt echt verwirrt. Überall steht, dass Char Arrays mit dem Specifier %s ausgelesen werden.

Ich versuche nochmal mein Problem zu schildern:
Der Code von oben funktioniert nicht richtig. Wenn ich den so lasse, einen Namen und ein Alter eingebe, dann wird das Alter trotz falschen short-Specifier angezeigt, aber der Name nicht. Wenn ich aber bei short ein * setze, also short*, dann wird der Name angezeigt. Was mich jetzt verwirrt, warum das so funktioniert, obwohl der short doch nichts mit dem Char-Array zutun hat.


C:
    char name[20];
    unsigned short age;

    printf("Hi there\n");
    printf("What's your name: "); //Eingabe: Test
    scanf("%s", &name);
    printf("And your age: "); //Eingabe: 20
    scanf("%u", &age);

    printf("Welcome %s to the first C program.\n", name); //%s bleibt leer
    printf("I see you're %u years old.\n", age); //trotz %u wird Alter angezeigt

    //Ausgabe:
    //Welcome  to the first C program.
    //I see you're 20 years old.
    return 0;
 

Jw456

Top Contributor
Char name[20] ist ein Array , name ist also ein Pointer

Also musst du bei scanf nur „name“ angeben und nicht „&name“

Code:
scanf("%s", name);

Bei dem primitiven Datentyp Short ist das was anderes. Da brauchst du die ref der Variablen.
denn scanf erwartet einen Pointer. (ref) auf den Buffer wo es die Eingabe ablegen kann.


Dein Problem ist bei scanf und nicht bei printf

Das ist ein typisches Beispiel von Pointer Fehlern in C deshalb hat ach Java auf die Pointer verzichtet.
 

KonradN

Super-Moderator
Mitarbeiter
Also ein char Array kann als String gewertet werden in c - dazu ist wichtig, dass der String mit 0x00 endet, denn wir haben in C null terminierte Strings.

Aber Du bist als Entwickler verantwortlich für den Speicher. Du lädst da z.B. eine Eingabe in ein Array von 20 Zeichen. Wenn Du da aber ein längeren Text eingibst, dann wird halt ein längerer Text geschrieben. Das bedeutet, dass man mit längeren Texten Speicher überschreiben kann:

Machen wir einfach einmal ein kleines Beispiel:
C:
#include <stdio.h>

int main()
{
    char name[20];
    unsigned short age;
    char name2[20];

    printf("Hi there\n");
    printf("What's your name: "); //Eingabe: Test
    scanf("%s", &name);

    printf("name: %s\n", name); //%s bleibt leer
    printf("name2: %s\n", name2); //%s bleibt leer
    printf("I see you're %u years old.\n", age); //trotz %u wird Alter angezeigt

    return 0;
}

Und da geben wir nun einen ganz langen String ein:
asdfghjklqasdfghjklqasdfghjklq

Das sind 30 zeichen - also ganz klar deutlich über den 20 Zeichen.

Und was haben wir jetzt für eine Ausgabe?
Code:
C:\temp\test>test.exe
Hi there
What's your name: asdfghjklqasdfghjklqasdfghjklq
name: asdfghjklqasdfghjklqasdfghjklq
name2: ghjklq
I see you're 1618 years old.

==> Es wurde also massiv Daten überschrieben!

Das gleiche Problem tritt ansonsten auch auf, wenn Du falsche format specifier verwendest. Denn scanf geht ja danach um etwas einzulesen. Wenn Du dem scanf sagst: Lies eine 4Byte unsigned int ein, dann macht es genau das.

Wenn dahinter aber nur ein 2Byte unsigned char steckt, dann hast Du ein Problem. So einfach ist das!

Ich kann mir das Verhalten nicht ganz erklären. Kann es sein, dass Du die Variablen anders herum hattest? Also erst die Variable age und dann die Variable name? (Wobei evtl. auch dein Compiler die Variablen anders anordnen könnte, aber das wäre jetzt ungewöhnlich.)

Was kann dann passieren? Es werden wieder Daten überschrieben. Du hast dann ja im Speicher:

Code:
age   | name
XX XX | XX XX XX XX XX XX XX XX XX XX ...
Statt den 2 Byte werden nun 4 Byte geschrieben. Also Du hast dann in name "Otto" geschrieben, das sorgt dann für
Code:
age   | name
XX XX | 4F 74 74 6F 00 XX XX XX XX XX ...

Dann kommt die Eingabe der Zahl - nehmen wir einfach einmal 1 als Alter ... als 16 Bit Zahl wäre es 00 01 und als 32 Bit Zahl ist es 00 00 00 01.
Aber aufpassen - das ist jetzt nicht die umgedrehte Reihenfolge wie die Speicherdarstellung ... Es wird also jetzt ab dem Speicherplatz für age geschrieben: 01 00 00 00:

Code:
age   | name
01 00 | 00 00 74 6F 00 XX XX XX XX XX ...

Damit is das 00 sofort am Anfang - die Ausgabe zeigt einen leeren Namen.

Das ist also das ganz große Problem bei C und C++! Du als Entwickler bist verantwortlich für die korrekten Speicherzugriffe! Jeder Fehler überschreibt direkt Daten! Und das sind dannn übrigens auch die ganzen Angriffe, die es da so gibt! Das ist also auch aus Sicherheitsaspekten extrem wichtig!
 

KonradN

Super-Moderator
Mitarbeiter
Char name[20] ist ein Array , name ist also ein Pointer
Ja und nein.

C:
    char name[20];
    printf("%p\n", name);
    printf("%p\n", &name);

Code:
000000FEA51EFCF8
000000FEA51EFCF8

Es gibt also beides mal die Adresse des ersten Elements aus. Daher funktioniert der Code auch mit &. Wobei ich im Augenblick nicht sicher bin, ob es eine C++ Compiler Sache ist oder auch bei reinen C Compilern funktionieren würde.
 

KonradN

Super-Moderator
Mitarbeiter
Was man sich übrigens auch einmal richtig anschauen sollte: Die Adressen der einzelnen Variablen. Mir ist aufgefallen, dass die Zahl, die für das Alter eingegeben wurde, nicht stimmig war - das entsprech nicht den Zeichen, die eingegeben wurde.

Und da habe ich dann gesehen: Microsoft Visual C++ packt age auch vor name:
0000001A450FF940 age
0000001A450FF948 name (+8 Bytes)
0000001A450FF960 name2 (+24 Bytes)

Das liegt aber mit daran, dass ich auf 64Bit übersetzt habe und der Compiler da auf die 8Byte ein Alignment der Variablen durchführt.

Mit einer 32Bit Übersetzung habe ich dann:
00CFFB68 age
00CFFB6C name (+ 4 Byte)
00CFFB80 name2 (+ 20 Byte)
==> Alignment auf 4 Byte (32 Bit)

Damit hast Du ein Verhalten, das vom Compiler/Linker sowie den verwendeten Flags abhängt. Ich werde deinen Fehler mit dem Überschreiben von name bei mir auf Windows mit Visual Studio 2022 nicht so einfach nachstellen können.
 

Jw456

Top Contributor
Teste doch auch mal nach der Eingabe namen mit scanf.
Gebe es mit printf aus. Das passt.
Jetzt kommt deine Alters Eingabe mir scanf %u und danach wird den namme nicht nehr ausgehen.
Wenn du eune int für das Alter benutzt ist es ok.
%u geht hier bei scanf wohl von einen int 4 Byte aus und wird etwas vom string überschreiben im Speicher.
 

KonradN

Super-Moderator
Mitarbeiter
Benutze mal einen online Compiler. Da konnte ich es nach stellen.
Das Problem sollte ja klar sein und wurde von mir ja erläutert. Daher ist es nicht wirklich notwendig, das noch weiter nachzustellen. Die Tatsache, dass Microsofts Compiler da ein Alignment hat ändert ja nichts daran.

Der Microsoft Compiler bringt ansonsten übrigens Warnungen, wenn man da falsche format specifier verwendet.
 

theqwe

Aktives Mitglied
Das sind 30 zeichen - also ganz klar deutlich über den 20 Zeichen.

Und was haben wir jetzt für eine Ausgabe?
C:\temp\test>test.exe
Hi there
What's your name: asdfghjklqasdfghjklqasdfghjklq
name: asdfghjklqasdfghjklqasdfghjklq
name2: ghjklq
I see you're 1618 years old.

==> Es wurde also massiv Daten überschrieben!

Das Phänomen lässt sich bei mir leider nicht reproduzieren. Liegt das eventuell am Compiler? Der Name und das Alter bleiben leer bei der Eingabe über 20 Zeichen. Habe den Code 1:1 so kopiert und exakt die gleiche Zeichenkette.

Ich kann mir das Verhalten nicht ganz erklären. Kann es sein, dass Du die Variablen anders herum hattest? Also erst die Variable age und dann die Variable name? (Wobei evtl. auch dein Compiler die Variablen anders anordnen könnte, aber das wäre jetzt ungewöhnlich.)

Genau wie der Code oben habe ich diese Reihenfolge so gehabt. Erst Name dann Alter. Ich kann den Fehler immer wieder produzieren. Vielleicht ist es interessant zu erwähnen, dass ich den g++ Compiler nutze. Das ganze Verhalten des Programmes ist einfach seltsam. Besonders wenn man jetzt das Pointer-Symbol an den short hängt. Dann wird der Name ja angezeigt.

Was kann dann passieren? Es werden wieder Daten überschrieben. Du hast dann ja im Speicher:

Code:
age   | name
XX XX | XX XX XX XX XX XX XX XX XX XX ...
Statt den 2 Byte werden nun 4 Byte geschrieben. Also Du hast dann in name "Otto" geschrieben, das sorgt dann für
Code:
age   | name
XX XX | 4F 74 74 6F 00 XX XX XX XX XX ...

Dann kommt die Eingabe der Zahl - nehmen wir einfach einmal 1 als Alter ... als 16 Bit Zahl wäre es 00 01 und als 32 Bit Zahl ist es 00 00 00 01.
Aber aufpassen - das ist jetzt nicht die umgedrehte Reihenfolge wie die Speicherdarstellung ... Es wird also jetzt ab dem Speicherplatz für age geschrieben: 01 00 00 00:

Code:
age   | name
01 00 | 00 00 74 6F 00 XX XX XX XX XX ...

Damit is das 00 sofort am Anfang - die Ausgabe zeigt einen leeren Namen.

Danke für die super ausführliche und verständliche Erklärung. Das leuchtet einen jetzt viel mehr ein, wenn man versteht, was intern auch passiert. Mit den Pointer ist für mich erstmal neu, trotzdem interessiert es mich ein brennend. Ich denke mal, dass der Umstieg zu C von Java erstmal etwas Zeit braucht, weil Java einen selbstverständlich vieles abnimmt. Weil ich aber auch gerne verstehen möchte, wie eine Middle-Level Language funktioniert und auch mal die "Anfangszeiten" der Programmierung nachvollziehen möchte, habe ich mich dafür mal entschieden.
 

Jw456

Top Contributor
Das Problem sollte ja klar sein und wurde von mir ja erläutert. Daher ist es nicht wirklich notwendig, das noch weiter nachzustellen. Die Tatsache, dass Microsofts Compiler da ein Alignment hat ändert ja nichts daran.

Der Microsoft Compiler bringt ansonsten übrigens Warnungen, wenn man da falsche format specifier verwendet.
Dann teste es halt nicht. Du bist doch sonnt der, der alles testen will.



Ich habe nur gesagt das ich sein Problem mit dem online Compiler
Auch beobachten konnte.

 

theqwe

Aktives Mitglied
Char name[20] ist ein Array , name ist also ein Pointer

Also musst du bei scanf nur „name“ angeben und nicht „&name“

Code:
scanf("%s", name);

Bei dem primitiven Datentyp Short ist das was anderes. Da brauchst du die ref der Variablen.
denn scanf erwartet einen Pointer. (ref) auf den Buffer wo es die Eingabe ablegen kann.


Dein Problem ist bei scanf und nicht bei printf

Das ist ein typisches Beispiel von Pointer Fehlern in C deshalb hat ach Java auf die Pointer verzichtet.

Das habe ich jetzt auch mal versucht und den Specifier des Alters falsch auf %u gelassen, anstatt %hu. Auch da bleibt der Name leer. Setze ich es auf %hu funktioniert es. Scheinbar geht wirklick mit &name, als auch nur name.
 

Jw456

Top Contributor
Mit & oder ohne ist in disem fall auch wirklich gleich da hat Konrad recht.

Mit einen int ob unsinnig onder nicht ging mit allen Compiler
die ich getestet habe.

Int für das Alter statt short
 

KonradN

Super-Moderator
Mitarbeiter
Genau wie der Code oben habe ich diese Reihenfolge so gehabt. Erst Name dann Alter. Ich kann den Fehler immer wieder produzieren.
Die Reihenfolge wird nicht zwingend durch den Source vorgegeben. Bei mir lag age im Speicher ja auch vor name. Du kannst ja auch einfach mal die Adressen der Variablen ausgeben.

Ansonsten habe ich derzeit kein Linux System mit g++ zum testen. Wobei Du bei reinem C Code evtl. auch den gcc nutzen willst - der hat dann weniger C++ Lasten dabei (Siehe https://stackoverflow.com/a/172592/11484412).

Das ändert aber nicht zu viel an der Problematik.

Das name nach dem schreiben von age leer scheint, wird an der oben genannten Erklärung liegen. Und Du könntest ja z.B. auch einmal die Zeichen alle ausgeben. Dann solltest Du sehen, dass da nur die ersten Zeichen überschrieben worden sind.

Und Du solltest eine Sache gelernt haben: Nimm immer den richtigen format specifier! Ansonsten kannst Du schnell in Teufels Küche kommen, weil Du Speicherbereiche schreibst, die nicht deiner Variablen entsprechen! Welche es gibt und was die Bedeutung jeweils ist, wurde Dir ja verlinkt:
(Bzw. da interessant der weiterführende Link zu https://cplusplus.com/reference/cstdio/printf/)

Und da hast Du dann eine Tabelle mit dem length sub specifier: h ist dabei für ein short. Und das hast Du, also ist es schon einmal ein h. Und dann hast Du ein unsigned Wert, also kommt da noch ein u. %hu ist die Angabe bei einem unsiged short (bzw. %ho oder %hx falls Du es nicht dezimal sondern oktal oder hexadezimal haben willst).

Also druck Dir die Tabelle aus und halte sie parat! Oder lerne sie, wenn Du es aus dem Kopf wissen musst. Dann kannst Du in der Tabelle schauen, was für einen Typ du hast (z.B. unsigned short int - kurz unsigned short) um dann erst nach Links zu schauen: Was findet sich da als Anfang (wäre hier das 'h') und dann nach oben: Was habe ich da für Möglichkeiten (hier dann: u o x X) und dann hast Du die validen Möglichkeiten. Und das sind dann hier: %hu, %ho, %hx oder %hX.

Und egal ob es mit anderen Alternativen evtl. auch zu gehen scheint: Diese sind und bleiben falsch! Das es nicht "knallt" liegt einfach daran, dass da durch Zufall der Compiler / Linker etwas optimiert hat so dass es gut geht!
 

KonradN

Super-Moderator
Mitarbeiter
Was die Eingabe vom String angeht: Da gilt das natürlich auch!

a) Du hast die Möglichkeit, dass dir scanf den Speicher reserviert. Dann hast Du allerdings die Speicherverwaltung an der Backe und Du musst schauen, wie es Deine Library genau will.

b) Wenn Du den Speicher selbst reserviert hast, dann solltest Du sicher gehen, dass da nicht drüber geschrieben wird. Dazu kannst Du statt %s die Länge mit angeben. Du hast 20 Zeichen in dem Array, das letzte ist aber ein \0, daher können 19 Zeichen eingegeben werden:
scanf("%19s", name);

Das war zwar kein Fehler, über den Du gestolpert bist (Weil Du es nicht getestet hast) aber es war halt auch ein klarer Fehler in Deinem Code (den ich bei meinen Tests verwendet hatte).
 

theqwe

Aktives Mitglied
So, vielen Dank euch beiden für die vielen Tipps. Die heutige Lektion Pointer und eine Menge Spielerei im Code hat mir entgültig den Ah-Effekt gegeben. Nun ließ sich auch der Overflow nachstellen bei mir nachstellen, sodass ein längerer Name das Alter überschreibt. Auch war es mit möglich, von der age-Adresse auf einzelne Buchstaben der name-Adresse zuzugreifen. Das hat mir beim Verständnis der Datentyp Größen und der Speicherverwaltung viel gebracht. Das kennt man natürlich von Java alles nicht.

scanf("%19s", name);
Das muss ich mir umbedingt merken. Das fehlte mir noch wie ich eine verlängerte Eingabe abfange.

Das ich von Anfang an mit den richtigen Specifier arbeiten soll, ist selbstverständlich. Es hat mich als Anfänger nur sehr verwirrt, warum ein andere Datentyp, den anderen beeinflussen kann. Das ist mir jetzt klar.
Mit & oder ohne ist in disem fall auch wirklich gleich da hat Konrad recht.

Mit einen int ob unsinnig onder nicht ging mit allen Compiler
die ich getestet habe.

Int für das Alter statt short
Im Tutorial wurde auch mit einem int gearbeitet. Da ich aber von Java weiß, dass man z.B. bei Alter nicht unbedingt einen großen Datentyp braucht (bei Java Byte), wollte ich dort auch den kleinsten nutzen. Und da tratt das Phänomen auf.

Mir ist noch aufgefallen, dass mein genutzter Compiler die Adressen der Variabeln von unten nach oben verteilt. Also in meinem Beispiel liegt die age Adresse vor dem Namen. Das macht dieser mit sämtlichen.
 

Jw456

Top Contributor
Wie du in der dir schon geben Tabelle sehen kannst
https://cplusplus.com/reference/cstdio/printf/)
ist

int %d
short int %hd
unsigned short int %hu
long %ld
....

ein "int" braucht kein erstes Zeichen


Mir ist noch aufgefallen, dass mein genutzter Compiler die Adressen der Variabeln von unten nach oben verteilt. Also in meinem Beispiel liegt die age Adresse vor dem Namen. Das macht dieser mit sämtlichen.
Er arbeitet hier mit dem Stack und benutzt es auch so.
Er pusht erst das Array und dann das Short
Also ist das Short oben auf dem Stapel. Wenn du nun mit „%u“ einen „int“ auf die Adresse giebst (32/64 Bit) wirt etwas vom Array überschreiben.
Short ist ja nur 16 Bit deshalb %hu dann weis der Compiler das es nur 16 Bit sind.
 
Zuletzt bearbeitet:

Neue Themen


Oben