ubuntuusers.de

c++ Anfaengerfrage(n)

Status: Gelöst | Ubuntu-Version: Lubuntu 16.04 (Xenial Xerus)
Antworten |

Zausel76

Avatar von Zausel76

Anmeldungsdatum:
23. Juni 2014

Beiträge: 30

Wohnort: Köln

Hi! Ich bin neulich auf Stackoverflow ueber eine Frage mit folgendem Code-Schnipsel gestolpert:

1
2
3
4
5
6
7
int main()
{
    char *p = "Hello";
    while(*p++)
         printf("%c",*p);
    return 0;
}

Der Output ist: ello

Was ich mich zuerst gefragt habe ist, wie und warum aus dem Ausdruck (*p++) ein false wird und dann ist mir aufgefallen, dass ich nicht einmal die erste Zeile

1
char *p = "Hello";

richtig verstehe.

Mein g++ gibt dazu auch eine Warnung, aber er uebersetzt es. Anhand vom Output und der Definition char *p erkenne ich, dass *p ein Pointer auf den char 'H' ist.

Ich koennte ja noch verstehen, dass das Stringliteral "Hello" aus Gruenden der Programmiererbequemlichkeit in der Initialisierung automatisch zu *p = 'H' umgewandelt wird. Aber wenn es so waere, dann wuerde ich erwarten, dass die while-Bedingung (*p++) fruehestens dann zu false evaluiert, wenn der Pointer p in char grossen Schritten an den Rand meines Arbeitsspeichers iteriert wurde.

Eine andere Erklaerungsmoeglichkeit fuer den Output waere vielleicht, dass p durch die Initialisierung automatisch zu einem Pointer zu einem Array aus chars wird. Ich wuerde dann zwar immer noch nicht verstehen, welcher magische Mechanismus (*p++) zu false evaluiert, aber dass eine Schleife nach dem letzten Index eines Arrays endet, waere grundsaetzlich immerhin ein vertrautes Prinzip. Viel verstoerender faende ich an diesem Erklaerungsansatz, dass er fordert, dass aus der Deklaration char *p automatisch ein Pointer zu einem Array aus chars wuerde.

Entschuldigt meine laienhafte Ausdrucksweise, aber ich bin ernsthaft interessiert, was in diesem Beispiel tatsaechlich vor sich geht.

LG

ChickenLipsRfun2eat Team-Icon

Anmeldungsdatum:
6. Dezember 2009

Beiträge: 12067

Hallo!

1
char *p = "Hello";

initialisiert einen pointer auf ein konstantes char array. Intern sieht das so aus:

H e l l o
0 1 2 3 4

p* zeigt also nach der Initialisierung auf p[0] und wird bis zum Endzeichen erhöht. Bei p[5] wird der Pointer ungültig und die Schleife abgebrochen. Das "H" verschluckt er, weil der Pointer ja bereits auf p[0] zeigt und durch die Erhöhung im Schleifenkopf auf p[1] gesetzt wird. Da müsste dann eine do...while-Schleife her.

Um das ganze Wort auszugeben, könnte man das so machen (hier auch gleich mit Zahleninfo)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>

int main()
{
    char *p="Hallo";
    do {
         printf("%c (%i)\n",*p, *p);
    } while(*p++);

    return 0;
}

Nachtrag: Ausgabe sieht dann bei mir so aus:

1
2
3
4
5
6
7
8
9
schiggn@x220arch [test$] gcc main.c
schiggn@x220arch [test$] ./a.out 
H (72)
a (97)
l (108)
l (108)
o (111)
 (0)
schiggn@x220arch [test$] 

Bearbeitet von rklm:

Arrayindizierung gerettet

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13174

ChickenLipsRfun2eat schrieb:

initialisiert einen pointer auf ein konstantes char array. Intern sieht das so aus:

H e l l o
0 1 2 3 4

Man sollte noch erwähnen, dass hinter dem "o" ein Nullbyte steht. Das ist die Konvention in C für Zeichenketten. Und nur so bricht die Schleife auch überhaupt ab (0 wird in C als "falsch" behandelt).

p* zeigt also nach der Initialisierung auf p[0] und wird bis zum Endzeichen erhöht. Bei p[5] wird der Pointer ungültig und die Schleife abgebrochen.

Das ist falsch. Der Zeiger ist noch gültig. Der Abbruch entsteht dadurch, dass an der Stelle im Speicher NUL steht und das Dereferenzieren im Schleifenkopf diese 0 zurückliefert, wie ja auch Dein Programm zeigt.

sebix Team-Icon

Moderator, Webteam

Anmeldungsdatum:
14. April 2009

Beiträge: 5582

Zausel76 schrieb:

Was ich mich zuerst gefragt habe ist, wie und warum aus dem Ausdruck (*p++) ein false wird

Ist er nicht. Sonst wuerde die Schleife nie begonnen werden. *p ist true, und dann wird *p zu *(p+1) und somit ueberspringst du das erste Zeichen.

Ich koennte ja noch verstehen, dass das Stringliteral "Hello" aus Gruenden der Programmiererbequemlichkeit in der Initialisierung automatisch zu *p = 'H' umgewandelt wird.

Ja, aber p ist ein Pointer, der Wert an der Stelle von p ist 'H'.

Aber wenn es so waere, dann wuerde ich erwarten, dass die while-Bedingung (*p++) fruehestens dann zu false evaluiert, wenn der Pointer p in char grossen Schritten an den Rand meines Arbeitsspeichers iteriert wurde.

Falsch, dafuer gibts das Nullbyte, wie von rklm erklaert.

Eine andere Erklaerungsmoeglichkeit fuer den Output waere vielleicht, dass p durch die Initialisierung automatisch zu einem Pointer zu einem Array aus chars wird.

Nicht magisch, aber ja.

Ich wuerde dann zwar immer noch nicht verstehen, welcher magische Mechanismus (*p++) zu false evaluiert,

Tut es nicht, siehe oben. Es wird der Wert an der Stelle im Speicher betrachtet und alles != \0 ist eben wahr.

aber dass eine Schleife nach dem letzten Index eines Arrays endet, waere grundsaetzlich immerhin ein vertrautes Prinzip.

Der Wert im Speicher muss unwahr sein, was auf das Nullbyte \0 zutrifft und dieses ist am Ende des Strings (implizit).

Viel verstoerender faende ich an diesem Erklaerungsansatz, dass er fordert, dass aus der Deklaration char *p automatisch ein Pointer zu einem Array aus chars wuerde.

Ist aber so.

ChickenLipsRfun2eat Team-Icon

Anmeldungsdatum:
6. Dezember 2009

Beiträge: 12067

rklm schrieb:

Das ist falsch. Der Zeiger ist noch gültig. Der Abbruch entsteht dadurch, dass an der Stelle im Speicher NUL steht und das Dereferenzieren im Schleifenkopf diese 0 zurückliefert, wie ja auch Dein Programm zeigt.

Stimmt. Falsch formuliert. Ungültig wäre ja ein segfault ☺ Danke fürs korrigieren.

Neral

Anmeldungsdatum:
3. Oktober 2007

Beiträge: 230

@Zausel76: Noch ein Hinweis: Die Warnung, den der g++ ausgibt, bedeutet, dass "Hallo" den Typ const char[] hat. Das an ein char * zuzuweisen ist ein Fehler. Und es ist undefiniert, was passiert, wenn man versucht, diesen String zu ändern. Sollte man also beides nicht tun.

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6488

Wohnort: Hamburg

Im Prinzip wurde zwar schon alles gesagt, aber eben noch nicht von jedem 😉

Da ich mit Zeilen immer etwa geizig bin, schlage ich mal folgende Änderung vor.

1
2
3
4
5
6
7
8
#include <cstdio>
int main()
{
    const char *p = "Hello\n"; // '\n' ergänzt
    while(*p)
         printf("%c",*p++); // <<<<
    return 0;
}

Der Zeiger p wird erst nach dem letzten Zugriff erhöht.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

Wobei man vielleicht noch sagen könnte/sollte, das es sich nicht wirklich im C++-Code handelt, sondern um C-Code der mit einem C++-Compiler übersetzt wird. In C++ würde das eher so aussehen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>
#include <string>

using namespace std;
int main()
{
    string text = "Hello";
    for (string::iterator it = text.begin(); it != text.end(); ++it) {
        cout << *it << endl;
    }
    return 0;
}

Neral

Anmeldungsdatum:
3. Oktober 2007

Beiträge: 230

In modernem C++ sieht das ganze dann so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <string>
#include <iostream>

int main()
{
    std::string text{"Hallo"};
    for (const auto &c : text) {
        std::cout << c;
    }
}

Wobei „modern“ relativ ist: Die range-based for-Schleife ist mittlerweile auch sechs Jahre alt, das return 0; ist schon immer optional.

Zausel76

(Themenstarter)
Avatar von Zausel76

Anmeldungsdatum:
23. Juni 2014

Beiträge: 30

Wohnort: Köln

Hi!

Danke fuer eure Antworten. Mir sind dadurch wirklich ein paar Lichter aufgegangen. z. B. ist mir jetzt glasklar wann und warum (*p++) zu false wird.

Aber ich glaub, es ist keinem so wirklich aufgefallen, dass ich ein furchtbares Problem mit der Vorstellung hatte, dass bei oder nach der "Type Declaration" char *p ein char[] oder ein Pointer auf char[] ins Spiel kommt.

Wenn ich das jetzt richtig verstanden habe -ihr duerft mich gerne korrigieren- hat das const char[] auch nichts mit der Typendeklaration vom Pointer p zu tun. Sondern const char[] ist das Stringliteral "Hello" und wird irgendwann zur Laufzeit im Arbeitsspeicher abgelegt. Nachdem dann der Pointer p initialisiert wurde, zeigt er auf das erste Bit vom const char[]. Aber nicht etwa, weil *p automatisch zu einem Pointer zu einem Array geworden waere. Sondern p ist und bleibt ein Pointer zu char. Weil dieser Character nunmal das 'H' aus "Hello" ist zeigt p gleichzeitig auf das erste Bit von char 'H' und auf das erste Bit von cont char[] Stringliteral.

Man sieht ja dann auch beim Output vom Inkrementieren, dass p um die Bitlaenge eines chars verschoben wird. (Was ich ziemlich clever geregelt finde, wenn man bedenkt, dass die character in meinem System-Zeichensatz UTF-8 unterschiedlich lang sind.) Ein Pointer zum const char[] wuerde hingegen beim Inkrementieren vermutlich um mindestestens 6*8 Bit verschoben werden. Aber in jedem Fall wuerde das einen anderen Output ergeben.

Fuer jemanden, der es so genau wissen will, kann ich mich leider nicht angemessen ausdruecken, aber ich hoffe, ich hab es jetzt verstanden und konnte auch noch klar machen, wo es bei mir gehakt hat.

LG

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11248

Wohnort: München

Zausel76 schrieb:

Man sieht ja dann auch beim Output vom Inkrementieren, dass p um die Bitlaenge eines chars verschoben wird. (Was ich ziemlich clever geregelt finde, wenn man bedenkt, dass die character in meinem System-Zeichensatz UTF-8 unterschiedlich lang sind.)

Eigentlich ist C(++) an der Stelle alles andere als intelligent (sondern tut nur, was man ihm sagt) und das Konstrukt funktioniert so auch nur garantiert korrekt für Zeichen, die zufällig die Größe von char haben (effektiv die Zeichen aus der altbekannten ASCII-Tabelle). Wenn du z.B. bei dem von dir am Anfang geposteten Schnipsel oder einer der anderen geposteten Varianten, die den String char für char zeilenweise ausgeben (wie z.B. in dem Beispiel), "Äonen" statt "Hello" nutzt, bekommst du für den Umlaut plötzlich Müll, weil das Multibyte-Zeichen aufgetrennt wird.

sebix Team-Icon

Moderator, Webteam

Anmeldungsdatum:
14. April 2009

Beiträge: 5582

Zausel76 schrieb:

Aber nicht etwa, weil *p automatisch zu einem Pointer zu einem Array geworden waere.

p ist ein Pointer auf das erste Element im Array. Inkrementierst du den Pointer, waechst er um die Groesse eines chars, also ein Byte! Bei anderen Type inkrementiert der Pointer nach deren Groesse. *p ist der Wert des in p abgelegten Werts.

Sondern p ist und bleibt ein Pointer zu char.

Siehe vorige Erklaerung, aber char ist jedenfalls ein Typ.

Weil dieser Character nunmal das 'H' aus "Hello" ist zeigt p gleichzeitig auf das erste Bit von char 'H' und auf das erste Bit von cont char[] Stringliteral.

Ja.

Man sieht ja dann auch beim Output vom Inkrementieren, dass p um die Bitlaenge eines chars verschoben wird. (Was ich ziemlich clever geregelt finde, wenn man bedenkt, dass die character in meinem System-Zeichensatz UTF-8 unterschiedlich lang sind.)

p ist ein char-Pointer, der sich um 1 Byte inkrementiert.

Hier ein Beispiel, mit dem wir mit einem Integer-Pointer durchiterieren:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <string.h>
#include <stdio.h>

using namespace std;

int main() {
    char* p = "Hello World!";
    int* q;
    q = (int*) p;
    do {
        printf("%x %c (%i)\n", q, *q, (char)*q);
    } while (++q < (int*)(p + strlen(p)));
}

Ergibt:

400664 H (72)
400668 o (111)
40066c r (114)

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6488

Wohnort: Hamburg

... wenn man bedenkt, dass die character in meinem System-Zeichensatz UTF-8 unterschiedlich lang sind.)

Da must Du leider selber aufpassen. UTF-8 Zeichen dürfen bis zu 4 Bytes lang sein und viele Funktionen wissen das nicht. Aber so lange du die Bytes, wie bei printf(), einfach nur weiter schaufelst, macht das nichts.

Kritisch wird es erst, wenn du selber die Positionen (nicht Bytes) auszählen willst, Beispielsweise um einen Text zu formatieren. Da währen evtl. die Datentypen wchar_t, char16_t oder char32_t interessant.

Edit: gelöscht, da bin ich jetzt selber durcheinander gekommen.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

Wobei auch bei char32_t und Unicode nicht gilt das ein Wert = ein Zeichen ist, auch dort kann man mehr als einen char32_t-Wert pro Zeichen haben. Text formatieren ist also grundsätzlich nicht einfach und man sollte sich entsprechende Bibliotheken dafür suchen.

Antworten |