Forum: PC-Programmierung C pointer syntax


von Reiner W. (reiner_w)


Lesenswert?

Nachdem mir hier - Beitrag "Grösse eines Array of pointers" 
schon so toll geholfen wurde, hab ich nochmal ne Frage an die 
Spezialisten.

Zur Ausgabe einer string constanten habe ich 2 Varianten probiert. Beide 
funktionieren erwartungsgemäß.

(Der printf ist nur zum Test. Im Original steht da eine Funktion, die 
zeichenweise ausgeben muss.)

1.
1
void sendString(char* str) {
2
    do {
3
       printf("%c",*str);
4
    }  while (*str++ = *(str+1));
5
}

2.
1
void sendString(char* str) {
2
    int i = 0;
3
    while (*(str + i)) {
4
        printf("%c", *(str + i));
5
        i++;
6
    }
7
}

Variante 1 spuckt aber eine Reihe Compilerwarnungen aus, die ich nicht 
wegbekomme.
1
test.cpp:23:22: warning: using the result of an assignment as a condition without parentheses
2
      [-Wparentheses]
3
    }  while (*str++ = * (str+1));
4
              ~~~~~~~^~~~~~~~~~~
5
test.cpp:23:19: warning: unsequenced modification and access to 'str' [-Wunsequenced]
6
    }  while (*str++ = * (str+1));
7
                  ^       ~~~

Ja, ihr könnt sagen, steht doch alles da. Aber ich kapier's trotzdem 
nicht und bekomme die Warnung nicht weg.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Reiner W. schrieb:

> Ja, ihr könnt sagen, steht doch alles da. Aber ich kapier's trotzdem
> nicht und bekomme die Warnung nicht weg.

In der Tat steht alles da. ;-)

> (Der printf ist nur zum Test. Im Original steht da eine Funktion, die
> zeichenweise ausgeben muss.)

(Es gibt auch putchar() ;-)

>     }  while (*str++ = *(str+1));

> test.cpp:23:22: warning: using the result of an assignment as a
> condition without parentheses

Nun, in der Flüchtigkeit vergisst manch einer schnell mal, bei einem 
Vergleich das doppelte Gleichheitszeichen zu schreiben:
1
    if (i = 42) { printf("hurra!\n"); }  // soll i == 42 sein

Das ist zwar syntaktisch korrekt (es wird eine Zuweisung ausgeführt, 
deren Wert dann die Bedingung darstellt), aber halt semantisch in aller 
Regel nicht das, was der Programmierer wollte.

Daher muss man eine Zuweisung innerhalb einer Steuerstruktur explizit 
machen, um die Warnung zu vermeiden. Entweder:
1
     }  while ((*str++ = *(str+1)) != 0);

oder
1
     }  while ((*str++ = *(str+1)));

Die doppelt notierten Klammern haben sich eingebürgert für "ja, das soll 
wirklich eine Zuweisung sein, deren Wert hier benutzt wird".


> test.cpp:23:19: warning: unsequenced modification and access to 'str'
> [-Wunsequenced]
>     }  while (*str++ = * (str+1));
>                   ^       ~~~

Die Reihenfolge, in der jeweils das Dererenzieren des Zeigers und das 
Inkrementieren (++) erfolgen, ist durch den Standard nicht 
festgeschrieben. Daher ist das Resultat der Aktion so genanntes 
"undefined behaviour". Sowas darf man einfach nicht in eine Zeile 
schreiben, sondern es muss sich zwischen beiden Aktionen ein so 
genannter "sequence point" befinden. Es gibt eine Reihe von definierten 
sequence points in C; der einfachst davon ist das abschließende 
Semikolon hinter einer Anweisung.

von Reiner W. (reiner_w)


Lesenswert?

Herzlichen Dank für die super Erklärung! Und deine Mühe!

Jörg W. schrieb:
> (Es gibt auch putchar() ;-)

Ich sag ja war nur ein Testschnipsel, ich kämpfe halt immer noch mit dem 
Pointerverständnis und probiere deshalb alles Mögliche aus.
Aber es wird schon besser ;-)

Das mit den doppelten Klammern habe ich inzwischen schon rausgefunden. 
Aber du hast die perfekte Erklärung nachgeliefert.
Nichts frustriert mich mehr, als wenn etwas klappt und ich nicht weis 
warum ;-)

von Stringpointer (Gast)


Lesenswert?

Reiner W. schrieb:
> Nachdem mir hier - Beitrag "Grösse eines Array of pointers"
> schon so toll geholfen wurde, hab ich nochmal ne Frage an die
> Spezialisten.
>
> 1.void sendString(char* str) {
>     do {
>        printf("%c",*str);
>     }  while (*str++ = *(str+1));
> }
>

Warum willst Du das überhaupt so machen? Kopiert man dann mit "while 
(*str++ = *(str+1))" nicht den Wert des nächsten Zeichens auf das 
derzeitige Zeichen?

von Reiner W. (reiner_w)


Lesenswert?

Stringpointer schrieb:
> Warum willst Du das überhaupt so machen?

Sry, da muss ich mich entschuldigen. Ich mach das nicht so, die Fragen 
dienten eher dazu etwas von meinem Pointernebel im Hirn zu lichten;-)
Nach dem Motto, dass müßte eigentlich auch so gehen...
Tatsächlich mach ich das so:
1
void sendString(char* str) {  
2
    while (*str) {            
3
     // do something with the char
4
     printf("%c", *str);
5
     
6
        str++;  // next char in string
7
    }
8
}
Wobei statt des printf eine func angesprungen wird, die zeichenweise auf 
nem STM eine PS2-Tastatur emuliert.

Stringpointer schrieb:
> Kopiert man dann mit "while
> (*str++ = *(str+1))" nicht den Wert des nächsten Zeichens auf das
> derzeitige Zeichen?

Jepp, aber das passiert ja innerhalb der func und der orinale const 
string merkt da nichts von.
Deswegen wird immer noch der selbe string ausgegeben, wenn ich die 
function mehrfach aufrufe.

Aber wie gesagt, ich bin nicht so oft in c unterwegs und deshalb will 
ich mich da lieber mit meinem (vlt. gefährlichen) Halbwissen 
zurückhalten.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Reiner W. schrieb:
> void sendString(char* str) {
>     while (*str) {
>      // do something with the char
>      printf("%c", *str);
>
>         str++;  // next char in string
>     }
> }

Der übliche Weg, die doppelte Dereferenzierung des Zeiges zu vermeiden, 
wäre eine Hilfsvariable:
1
  char c;
2
  while ((c = *str) != '\0') {
3
     // do something with the char
4
     printf("%c", c);
5
6
     str++;  // next char in string
7
  }

Derartige Beispiele finden sich schon im guten alten Kernighan/Ritchie. 
;-)

Dort wird auch gezeigt, dass folgende Verkürzung OK ist:
1
  char c;
2
  while ((c = *str++) != '\0') {
3
     // do something with the char
4
     printf("%c", c);
5
  }

Beachte, dass anders als in deinem Code hier der Zeiger nur einmal 
dereferenziert wird, und das zugegriffene Objekt (vom Typ "char") wird 
auch nur gelesen (was immer eine gute Idee ist in so einem Falle).

Dass man das im K&R als "Mikro-Optimierung" so benutzt hat, hängt u.a. 
damit zusammen, dass die PDP-11 (auf der C entwickelt worden ist), für 
*p++ und *--p nativ eigene Adressierungsmodi besaß (die letztlich dort 
auch für den Stack-Zugriff benutzt worden sind). Das dürfte primäre 
Motivation gewesen sein, solche Ausdrücke auch in C zuzulassen, denn 
damit ließen sich mit der eher bescheidenen Optimierung damaliger 
Compiler trotzdem noch die Möglichkeiten der Maschine ausnutzen. 
Heutzutage ist das den Compilern natürlich herzlich egal, ob du das 
Dereferenzieren des Zeigers und das Inkrementieren so "schön C-mäßig" in 
einen Ausdruck setzt oder im Code aufteilst.

von Reiner W. (reiner_w)


Lesenswert?

Jörg W. schrieb:
> Derartige Beispiele finden sich schon im guten alten Kernighan/Ritchie.
> ;-)

Ja, immer drauf ;-) Ist alles schon solange her...
Also wenn ich deine Ausführungen richtig verstehe, ist der code nicht 
old scool, gibt aber keinen zwingenden Grund eine extra Variable zu 
investieren? Jedenfalls meckert mein compiler auch nicht.

Jörg W. schrieb:
> und das zugegriffene Objekt (vom Typ "char") wird
> auch nur gelesen (was immer eine gute Idee ist in so einem Falle).

Ja, das Argument kapier ich. Danke.

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

Reiner W. schrieb:
> Also wenn ich deine Ausführungen richtig verstehe, ist der code nicht
> old scool, gibt aber keinen zwingenden Grund eine extra Variable zu
> investieren?

Die Konstellation, dass man ein Zeichen bis zur 0 auslesen und 
verwenden will, und anschließend den Ptr erhöhen, ist in C immer 
unbefriedigend.

Entweder hat man eine Zuweisung in einer Bedingung, z.B.:
1
while((c=*ptr++)!=0){
oder man hat die Zuweisung doppelt, z.B.:
1
for(c=*ptr++; c; c=*ptr++){
oder man hat die Abfrage doppelt
1
do {
2
    if(*ptr).. doSomething(*ptr);
3
}while(*ptr++);

Dein Erster Code ist doppelt falsch:
a) Er manipuliert die Quelle und
b) testet das erste Zeichen nicht.

Ohne a) wäre es einfach:
1
void sendString(char* str) {
2
    do {
3
       printf("%c",*str);
4
    }  while (*++str);
5
}
Das geht lange gut, bis irgendwann mal ein String von 0 Zeichen Länge 
kommt.

Einfach wird es, wenn die 0 auch mit ausgegeben wird:
1
void sendString(char* str) {
2
    do {
3
       printf("%c",*str);
4
    }  while (*str++);
5
}
Darum ist z.B. strcpy in der einfachsten Version sehr klein.

von Reiner W. (reiner_w)


Lesenswert?

A. S. schrieb:
> Das geht lange gut, bis irgendwann mal ein String von 0 Zeichen Länge
> kommt.

Ja, danke für die ausführlichen Ausführungen. Habs schon geändert.

von Reiner W. (reiner_w)


Lesenswert?

Jörg W. schrieb:
> Dort wird auch gezeigt, dass folgende Verkürzung OK ist:
>   char c;
>   while ((c = *str++) != '\0') {
>      // do something with the char
>      printf("%c", c);
>   }

Prinzipiell ja, allerdings verliere ich so das erste Zeichen, deshalb 
die do..while

von A. S. (Gast)


Lesenswert?

Reiner W. schrieb:
> Prinzipiell ja, allerdings verliere ich so das erste Zeichen, deshalb
> die do..while

äh, nein. Alle Versionen sind äquivalent zu Deiner zweiten. Du musst in 
der Schleife dann natürlich c nutzen und nicht *ptr.

Oder was möchtest Du genau? Wenn ein String "AB" mit abschließender 0 
kommt:

1) A, B
2) A, B, 0

von Reiner W. (reiner_w)


Lesenswert?

A. S. schrieb:
> dann natürlich c nutzen und nicht *ptr.

Sry, das hab ich übersehen. Wenn kopieren, dann auch komplett;-)

von Wilhelm M. (wimalopaan)


Lesenswert?

Das grundlegende Problem, was Du hier hast, und was die folgenden Fehler 
möglich macht, ist, dass Du hier in der Parameterliste einen sog. 
Output-Parameter (bzw. Input/Output) deklariert hast. Output-Parameter 
sollten aber die große Ausnahme sein. Du möchtest ja den C-String des 
Aufrufers nicht ändern.

Daher besser so:
1
void sendString(const char* str) { // das Zielobjekt ist read-only
2
    assert(str);
3
    do {
4
       printf("%c",*str);
5
    }  while (*str++ = *(str+1));
6
}

Das deckt die ungewollte Zuweisung sofort klar auf.

Zudem: der nullptr  der Wert NULL  0 gehört bei Deinem Code nicht zur 
zugelassenen Wertemenge des Zeiger-Parameters str. Wenn Du bei rohen 
Zeigern bleiben willst, dann solltest Du eine Vorbedingung in Form einer 
Zusicherung einbauen, um den Wertebereich von str einzuschränken.

Beide Maßnahmen helfen gerade Programmieranfängern dabei, den Code klar 
zu formulieren bzw. mehr über seine Wirkung zielgerichtet nachzudenken.

von Wilhelm M. (wimalopaan)


Lesenswert?

Abgesehen davon, sollten Parameterdefinitionen (und lokale Variablen, 
wenn möglich) immer read-only sein, damit ein schleichender 
Bedeutungswechsel der Variablen ausgeschlossen ist. Und dort, wo das 
nicht geht, sollte der Gültigkeitsbereich möglichst eingeschränkt 
werden.
1
void sendString(const char* const str) {
2
    assert(str);
3
    for(const char* iter = str; *iter != '\0'; iter += 1) {
4
        printf("%c", *iter);
5
    }
6
}

Hier ist str in der gesamten Funktion der C-String, den der Aufrufer 
übergeben hat, und nicht nur ein Teil davon.
Der Iterator iter ist auf die for-Schleife im Gültigkeitsbereich 
begrenzt und hat einen Namen, der seinen Zweck besser beschreibt.
Mache Dir keine Gedanken um Laufzeit, vertraue dem Compiler! 
Optimierungen sind die Aufgabe des Compilers, die er sehr gut kann, wenn 
Du präzise in Deiner programmatischen Aussage bist.

von Reiner W. (reiner_w)


Lesenswert?

Wilhelm M. schrieb:
> dass Du hier in der Parameterliste einen sog.
> Output-Parameter (bzw. Input/Output) deklariert hast.

Wilhelm M. schrieb:
> Zudem: der nullptr  der Wert NULL  0 gehört bei Deinem Code nicht zur
> zugelassenen Wertemenge des Zeiger-Parameters str.

Oh, man. Wußte gar nicht, dass ich in so ein kleines Code-Schnipsel so 
viele Fußangeln packen kann.

Wilhelm M. schrieb:
> Hier ist str in der gesamten Funktion der C-String, den der Aufrufer
> übergeben hat, und nicht nur ein Teil davon.
> Der Iterator iter ist auf die for-Schleife im Gültigkeitsbereich
> begrenzt und hat einen Namen, der seinen Zweck besser beschreibt.

Hab's kapiert, nun muss ich es nur noch verinnerlichen.

@all ursprüngliche Frage perfekt beantwortet, dazu noch viele sehr 
sinnvolle Tips bekommen, wie man es besser machen kann! Was will man 
mehr.

Zumindest in diesem Forum werde ich das wohl nie zurückgeben können.
Danke

: Bearbeitet durch User
von Stringpointer (Gast)


Lesenswert?

Ja, sehe ich genau so. Den String solltest Du nicht verändern und als 
"const char* str" belassen. Stringliterale habe immer diesen Typ. Da 
zeigt sich meist auch ein typischer Anfängerfehler, zum Beispiel bei der 
Übergabe an eine Funktion "void send(char* str)" konstante Strings nach 
"(char*)" zu casten "weil der Compiler sonst meckert", ohne zu wissen, 
warum er damit Probleme hat. Er wirft dort manchmal Warnungen, wenn man 
konstante Strings einer Funktion übergibt, die veränderliche Strings 
erwartet.

Wenn Du also eine Funktion schreibst, die den String nur ausgeben soll, 
dann sieht meine Lieblingsversion wie folgt aus:
1
void send(const char* str) {
2
    while(*str) {
3
        printf("%c", *str++);
4
    }
5
}

Das ist die einfachste Variante. Den Nullpointercheck, wie ihn Wilhelm 
M. per assert vorgeschlagen hat, kann man natürlich auch noch einfügen. 
Oder für Minimalcode-Fetischisten die while-Prüfung auf "while(str && 
*str)" erweitern und hoffen, dass der Compiler das ordentlich optimiert 
:D

Die hier dargestellte Variante würde ich sogar noch bevorzugen, da bei 
der Übergabe des Strings vor (!) der Ausgabe gecheckt wird, ob das 
Stringende a la \0 bereits erreicht ist. Die do-while-Schleifen geben 
immer ohne check das erste Zeichen aus (zum Beispiel übergabe von ""). 
Das kann je nach Schnittstelle hinderlich oder nur unschön sein.

Bei der Veränderlichkeit des Pointers selbst sehe ich prinzipiell keine 
Probleme, wenn Du den originalen Wert des Pointers nur einmal brauchst. 
Das spart Dir dann die Hilfsvariable. Für den Code  außerhalb der 
Funktion wird ja an dem übergebenen Variablen, also der Adresse des 
Strings, nichts verändert.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Reiner W. schrieb:
> gibt aber keinen zwingenden Grund eine extra Variable zu investieren?

Du "investierst" da nichts. Die Variable wird am Ende einfach nur ein 
CPU-Register sein.

Lös dich mal von dem Gedanken, dass das, was du da hinschreibst, auch 
nur annähernd 1:1 durch den Compiler auf die CPU umgesetzt wird. Das mag 
vor 45 Jahren auf einer PDP-11 so gewesen sein.

von Reiner W. (reiner_w)


Lesenswert?

Jörg W. schrieb:
> Das mag
> vor 45 Jahren auf einer PDP-11 so gewesen sein.

So ist das halt, wenn man das noch persönlich erlebt hat ;-)

von Wilhelm M. (wimalopaan)


Lesenswert?

Stringpointer schrieb:
> Den String solltest Du nicht verändern und als
> "const char* str" belassen. Stringliterale habe immer diesen Typ.

Das ist leider falsch: in C ist der Typ char[N] und in C++ ist er const 
char[N]. Daher zerfällt (decay) der Stringbezeichner dann in C zu char* 
und in C++ zu const char*.

Wenn man in C dann versucht ein Zeichen zu modifizieren, stellt man zur 
Laufzeit fest, das der Compiler das Literal in einer ro-section plaziert 
hatte.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Stringpointer schrieb:
> Das kann je nach Schnittstelle hinderlich oder nur unschön sein.

Nein, es ist einfach falsch. Denn das `\0` gehört zu dem C-String nicht 
dazu. Es ist nur das Sentinel.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Reiner W. schrieb:
> Oh, man. Wußte gar nicht, dass ich in so ein kleines Code-Schnipsel so
> viele Fußangeln packen kann.

Ja, das Problem an C ist, dass die Sprache in vielen Dingen "zu einfach" 
ist, und gerade dem Anfänger daher zu viel Verantwortung überlasst.

von A. S. (Gast)


Lesenswert?

Stringpointer schrieb:
> dann sieht meine Lieblingsversion wie folgt aus:
> void send(const char* str) {
>     while(*str) {
>         printf("%c", *str++);
>     }
> }
>
> Das ist die einfachste Variante.

Ich halte hier die Manipulation des Schleifenwertes in einer komplexen 
Anweisung irgendwo in der Schleife für unglücklich.

Also entweder explizit am Ende oder gleich als for(; *str; str++)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.