Zeiger in C



  • @Peter-Viehweger sagte in Zeiger in C:

    Ja man kann immer Warnungen aus dem Compiler "herauskitzeln". Die Warnung wird aber nur deshalb ausgegeben, weil mit -pedantic auf Standardkonformität geprüft wird und der Standard - wie du bereits erläutert hast - besagt, dass an printf beim Spezifizierer "%p" ein "void *" übergeben werden sollte. Das ist aber auch der einzige Grund.

    Du bist hier im C-Forum. Im Standard-C-Forum.


    @Peter-Viehweger sagte in Zeiger in C:

        n = read(fd, &f, sizeof(f));
    

    @Peter-Viehweger sagte in Zeiger in C:

    Muss ja eigentlich auch so sein, oder?

    Das ist ja auch keine Funktion mit einer Ellipse.

    ISO/IEC 9899:202x (E) § 6.5.2.2/7:

    If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. [...]



  • @Peter-Viehweger
    Es macht einen Unterschied ob du etwas an eine "normale" Funktion oder an eine "varargs" Funktion (so eine mit ", ...") im "varargs" Teil übergibst.

    Bei der Übergabe an eine normale Funktion wird z.B. auch eine Konvertierung long -> int oder long long -> int implizit durchgeführt.

    Bei der Übergabe im varargs Teil nicht. Und das kann dann zu echten Fehlern führen.
    Beispiel:

    #include <stdio.h>
    
    void int_fun(int i) {}
    
    int main()
    {
        long long ll = 42;
    
        int_fun(ll); // OK
        printf("%d, %d\n", ll, ll); // falsch
    
        return 0;
    }
    // Übliche Ausgabe auf 32 Bit Systemen: 42, 0
    

    Da braucht man dann auch kein -pedantic mehr um eine Warning zu bekommen.

    <source>:10:11: warning: format '%d' expects argument of type 'int', but argument 2 has type 'long long int' [-Wformat=]
       10 |  printf("%d, %d\n", ll, ll);
          |          ~^         ~~
          |           |         |
          |           int       long long int
          |          %lld
    <source>:10:15: warning: format '%d' expects argument of type 'int', but argument 3 has type 'long long int' [-Wformat=]
       10 |  printf("%d, %d\n", ll, ll);
          |              ~^         ~~
          |               |         |
          |               int       long long int
          |              %lld
    

    %p und nicht-void Zeiger ist hier ein Sonderfall, weil die Repräsentation von Zeigern im Speicher bzw. Registern üblicherweise für alle Zeiger gleich ist. D.h. es wird üblicherweise "trotzdem funktionieren". Das weiss der Compiler, und daher gibt er dir nur eine Warning mit -pedantic. Das heisst aber nicht dass es richtig ist.



  • @Peter-Viehweger sagte in Zeiger in C:

    Ja man kann immer Warnungen aus dem Compiler "herauskitzeln". Die Warnung wird aber nur deshalb ausgegeben, weil mit -pedantic auf Standardkonformität geprüft wird und der Standard - wie du bereits erläutert hast - besagt, dass an printf beim Spezifizierer "%p" ein "void *" übergeben werden sollte. Das ist aber auch der einzige Grund.

    Den muss ich mir merken. "Aaaaber...es gibt nur warnungen wenn man sie auch anmacht!!11"



  • @hustbaer sagte in Zeiger in C:

    Da braucht man dann auch kein -pedantic mehr um eine Warning zu bekommen.
    [...]
    %p und nicht-void Zeiger ist hier ein Sonderfall, weil die Repräsentation von Zeigern im Speicher bzw. Registern üblicherweise für alle Zeiger gleich ist. D.h. es wird üblicherweise "trotzdem funktionieren". Das weiss der Compiler, und daher gibt er dir nur eine Warning mit -pedantic. Das heisst aber nicht dass es richtig ist.

    Das weiß nicht nur der Compiler, sondern ich auch. Deshalb habe ich ja gesagt, dass es eigentlich Unsinn darstellt und den Code aufpustet, wenn vorher noch nach void umgewandelt wird.

    @Cardiac sagte in Zeiger in C:

    Den muss ich mir merken. "Aaaaber...es gibt nur warnungen wenn man sie auch anmacht!!11"

    Warnungen gibt es auch, wenn man kein -pedantic übergibt, nur die, dass man vorher doch bitte den Zeiger nach void * umwandeln soll, eben nicht......



  • @Peter-Viehweger sagte in Zeiger in C:

    Das weiß nicht nur der Compiler, sondern ich auch.

    Also erstmal würde ich behaupten dass du viel weniger weisst als du meinst zu wissen. Du hast ja selbst in diesem Thread schon demonstriert dass du eben nicht weisst was bei varargs Funktionen passiert. Siehe

    @Peter-Viehweger sagte in Zeiger in C:

    Der Compiler führt aber den impliziten Cast zu "void *" aus, weshalb so etwas nur unnötig den Code aufpustet.

    ...

    @Peter-Viehweger sagte in Zeiger in C:

    Deshalb habe ich ja gesagt, dass es eigentlich Unsinn darstellt und den Code aufpustet, wenn vorher noch nach void umgewandelt wird.

    Äh... nein. Klingt für mich eher so als ob du da was falsch verstanden gehabt hättest und es jetzt im Nachhinein schönreden willst. Denn es passiert eben genau kein impliziter Cast, es wird lediglich das Bitmuster vom float* als void* interpretiert. Das ist kein Cast sondern eher memcpy. Und wie schon geschrieben wurde eben UB.


    Und dann ist halt die Frage ob es gut ist sich auf "funktioniert trotzdem" bei undefiniertem Verhalten zu verlassen. Speziell wenn der einzige Vorteil davon ist dass man einen kleinen (laut Sprachstandard nötigen!) Cast nicht schreiben muss. Das macht mMn. keinen Sinn - sollte man sich nicht angewöhnen.



  • @hustbaer Du hast recht: Ich müsste jetzt im Internet nachlesen, wie das mit den varargs genau funktioniert, weil ich so etwas eigentlich nie benutzt habe, außer eben bei printf und scanf. Wenn ich mich aber richtig erinnere, müssen der Funktion "irgendwie" die Anzahl der Argumente und die jeweilige Größe bekannt gemacht werden.

    Prüfungsfrage: Worin besteht bei Zeigern der Unterschied zwischen einem Cast und einer Uminterpretation des Bitmusters?



  • @Peter-Viehweger sagte in Zeiger in C:

    Wenn ich mich aber richtig erinnere, müssen der Funktion "irgendwie" die Anzahl der Argumente und die jeweilige Größe bekannt gemacht werden.

    Das erfolgt über das erste Argument– dem Formatstring! Deshalb ist es ja so wichtig, da nicht sinnfreie Aktionen durchzuführen und entweder zu casten (es besteht die Gefahr etwas Falsches zu übergeben) oder Warnungen zu ignorieren.

    Nachtrag: Ein Beispiel mit funktionierendem Code, was verdeutlicht wie man in C mit Parameter Ellipsen arbeitet.

    #include <stdarg.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    int add_values (size_t num_arguments, ...) {
        int sum = 0, a = 0;
        va_list list_pointer;
        va_start (list_pointer, num_arguments);
    
        for (size_t i = 0; i < num_arguments; ++i) {
            a = va_arg(list_pointer, int);
            sum += a;
        }
    
        va_end(list_pointer);
    
        return sum;
    }
    
    int main () {
        int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 8;
    
        printf ("%i\n", add_values(3, a, b, c));
        printf ("%i\n", add_values(8, a, b, c, d, e, f, g, h));
    
        return EXIT_SUCCESS;
    }
    


  • @Peter-Viehweger sagte in Zeiger in C:

    Worin besteht bei Zeigern der Unterschied zwischen einem Cast und einer Uminterpretation des Bitmusters?

    Du versuchst also seit X Beiträgen gegen etwas zu argumentieren was du nicht verstehst. Gratuliere.



  • @Swordfish Vielleicht solltest du die Frage dann einfach einmal beantworten, anstatt mir ständig zu erzählen zu erzählen, wie ahnungslos ich bin.🤗



  • @Peter-Viehweger

    #include <limits.h>
    #include <memory.h>
    #include <assert.h>
    #include <stdio.h>
    
    void print_bits(void *value, size_t size)
    {
        for (size_t byte = size; byte; --byte) {
            for (size_t bit = CHAR_BIT; bit; --bit) {
                printf("%d", (((char*)value)[byte - 1] & (1 << (bit - 1))) != 0);
            }
        }
    }
    
    int main(void)
    {
        assert(sizeof(int) == sizeof(float));
    
        int foo = 1234567;
        float bar = foo;  // implicit typecast
        printf("%i %f\n", foo, bar);
        print_bits(&foo, sizeof(foo));
        putchar('\n');
        print_bits(&bar, sizeof(bar));
        putchar('\n');
    
        memcpy(&bar, &foo, sizeof foo);  // reinterpretation of bit pattern
        printf("%i %f\n", foo, bar);
        print_bits(&foo, sizeof(foo));
        putchar('\n');
        print_bits(&bar, sizeof(bar));
        putchar('\n');
    }
    


  • @Peter-Viehweger sagte in Zeiger in C:

    @hustbaer Du hast recht: Ich müsste jetzt im Internet nachlesen, wie das mit den varargs genau funktioniert, weil ich so etwas eigentlich nie benutzt habe, außer eben bei printf und scanf. Wenn ich mich aber richtig erinnere, müssen der Funktion "irgendwie" die Anzahl der Argumente und die jeweilige Größe bekannt gemacht werden.

    Ja, die Funktion muss das irgendwie wissen wenn sie wirklich auf die Parameter zugreifen will. Allerdings hilft ihr niemand dabei. Für die Funktion gibt es keinen Weg das herauszufinden, es sei denn sie erfindet selbst irgend ein "Protokoll" und schreibt dem Aufrufer vor sich genau an dieses zu halten. Die ...f Funktionen verwenden dazu den Format-String und daher muss man auch exakt zum Format-String passende Parameter (Anzahl und Typ) übergeben.
    Andere Funktionen in anderen Libraries verwenden andere Protokolle. Auf AmigaOS gibt es z.B. die ...Tags Funktionen die eine Key-Value Liste erwarten. Dabei wird immer abwechselnd Key und Value übergeben, so lange bis ein spezieller Wert als Key auftaucht der das Ende signalisiert. Die Keys sind dabei immer unsigned long und der Typ der Value hängt dann vom Key ab. Wenn der Aufrufer sich an das vorgeschriebene Muster hält, kann die Funktion dabei ebenso die genaue Anzahl und Typen der Parameter rekonstruieren.

    Prüfungsfrage: Worin besteht bei Zeigern der Unterschied zwischen einem Cast und einer Uminterpretation des Bitmusters?

    Unterschiedliche Zeiger-Typen dürfen laut Standard unterschiedliche Repräsentation (im Speicher bzw. auch Registern) haben. Also z.B. auch schonmal unterschiedliche Grösse. Manche Maschinen können nur 2- oder 4-Byte Worte adressieren. D.h. für einen char Zeiger sind dort zwei Werte nötig - die Adresse (welche die Wort-Nummer angibt) und z.B. die Byte-bzw. Bit-Nummer innerhalb des Wortes wo der char anfängt. Ein int Zeiger kommt allerdings mit einem einzigen Wert aus - einfach nur die Wort-Adresse. char und int Zeiger wären dann sehr unterschiedlich aufgebaut. Ein void Zeiger ist auf solchen Systemen immer so aufgebaut dass er sämtliche Adressen transportieren kann, also üblicherweise gleich wie ein char Zeiger.

    Wenn du jetzt einen Cast verwendest, dann garantiert dir die Sprache dass das ganze passend konvertiert wird, so dass das Ergebnis auf die selbe Adresse verweist wie der ursprüngliche Zeiger. Vorausgesetzt die Adresse ist im neuen Zeigertyp darstellbar.

    Wenn du dagegen memcpy oder einen Varargs-Zugriff verwendest dann werden eben einfach die Bits kopiert. Wobei dann schonmal das erste Problem ist dass die Zeiger nicht gleich gross sind. Bei einem Varargs-Zugriff wo die Funktion den grösseren Zeigertyp erwartet aber der kleinere übergeben wurde, werden also zusätzliche Bits gelesen die gar nicht übergeben wurden. Das kann dann

    • Das Ergebnis verändern
    • Alle folgenden Zugriffe verschieben, d.h. bei denen kommt dann auch nur mehr Mist raus
    • Zu einem Crash führen
    • ...

    Auf Systemen wo alle Zeiger gleich aufgebaut sind entspricht ein Cast dagegen normalerweise dem Uminterpretieren der Bits. D.h. in dem Fall gäbe es keinen Unterschied. Nur macht es keinen Sinn sich darauf zu verlassen bloss damit man einen Cast nicht schreiben muss.

    C und C++ haben viele solche Regeln die man meistens nicht braucht. Und wenn man sie alle ignoriert, dann hat man relativ schnell ein Programm welches sich auf bestimmte reale Systeme nicht mehr portieren lässt - oder nur mit riesigem Aufwand. Und wenn so ein System dann auf einmal relevant wird, dann weint man.

    Es gibt jetzt sicher schlimmeres als bei %p nicht zu casten. Aber zu behaupten es wäre richtig bzw. völlig egal ist falsch. Es ist weder richtig noch ist es völlig egal.



  • @hustbaer sagte in Zeiger in C:

    Unterschiedliche Zeiger-Typen dürfen laut Standard unterschiedliche Repräsentation (im Speicher bzw. auch Registern) haben. Also z.B. auch schonmal unterschiedliche Grösse.

    Die klassischen 8Bit Systeme wurden sobald die Speichergröße 64kB erreichten mit Bank Switching in unterschiedliche Banks aufgeteilt, die man nach und nach durch Umschalten spezieller Register in der CPU oder durch Schreiben auf magische Adressen umschalten konnte. Diese Computer wurden in der Regel noch nicht mit Hochsprachen wie etwa C programmiert, sondern meistens in Assembler. Zeiger sind auf diesen Plattformen 16Bit groß, die Informationen für das Bank Swiching musste selbst verwaltet werden.

    Auf 16Bit Systemen kamen dann die ersten Hochsprachen auf, z.B. C auf der DEC PDP-11 mit Hilfe dessen die Portierung von UNIX auf diese Maschine vereinfacht wurde. Für Anwendungen blieb die PDP-11 immer bei 16Bit, aber das System konnte mehrere solcher Programme parallel betreiben. Bei den 16Bit Systemen gab es dann auf diversen Plattformen Erweiterungen der 16Bit Adressregister, weil 64kB Adressraum auf den meisten kommerziellen Systemen viel zu klein war. Die bekannteste Krücke dürfte Intels 8088 im ersten IBM PC gewesen sein. Hier wird 16+4 Bit Adressierung genutzt, und es gibt spezielle Pointer (near und far) für die jeweiligen Adressierung, d.h. man kann die beiden Typen nicht einfach ineinander konvertieren. Das alles wäre nur vom historischem Interesse, wenn es nicht noch immer Microcontroller mit ganz ähnlichen Macken gäbe. Womit sich der Kreis zu aktuellem C wieder schließt.



  • @hustbaer sagte in Zeiger in C:

    C und C++ haben viele solche Regeln die man meistens nicht braucht. Und wenn man sie alle ignoriert, dann hat man relativ schnell ein Programm welches sich auf bestimmte reale Systeme nicht mehr portieren lässt - oder nur mit riesigem Aufwand. Und wenn so ein System dann auf einmal relevant wird, dann weint man.

    DAS! Also auf einen cast zu verzichten der in den meisten Fällten in einer NOP resultiert nur um sagen zu können "Ich bin viel schlauer als der Compiler" doesn't quite cut it.


Anmelden zum Antworten