ubuntuusers.de

Verständnisproblem mit Pointer

Status: Gelöst | Ubuntu-Version: Ubuntu Budgie 17.04 (Zesty Zapus)
Antworten |

momooo

Anmeldungsdatum:
11. Oktober 2017

Beiträge: Zähle...

Liebe Forenbesucher,

es könnte sein, dass die Frage eventuell für dieses Forum unpassend ist, aber ich wüsste nicht wo ich sonst nachfragen könnte. Wie im Betreff schon erwähnt, handelt es sich um Pointer.

Ich habe eine Funktion, in der ich durch ZMQ einen Socket erstelle. Diese Funktion gibt einen void Pointer zurück. Meine Idee war es, damit ich keine globale Variable verwenden muss, diesen Pointer durch call-by reference zu übergeben. Jedoch erhalte ich einen NULL - Pointer in der Funktion in der ich es aufrufe. Das Beispiel unten erklärt mein Problem hoffentlich besser:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
     /* Funktionsprototyp */
     void foo (void *test_pointer);

     int main(){
     
       void *ubergabe_pointer = NULL;

       foo(ubergabe_pointer);
       printf(" %p \n", übergabe_pointer);

     }

      void foo (void *test_pointer){
     
         test_pointer = zmq_socket(...);

      }

Die Funktion zmq_socket() gibt einen void-Pointer und damit ich diese nicht in eine globale Variable speichern will, wollte ich diese über einen Pointer zurück geben, nur bekomme ich immer NULL.

Ist das eigentlich möglich oder muss ich es über eine globale Variable erledigen? LG

Vain

Avatar von Vain

Anmeldungsdatum:
12. April 2008

Beiträge: 2510

Du musst da zweifache Indirektion verwenden.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void
fill_foo(char **foo_p)
{
    *foo_p = malloc(4);
    strcpy(*foo_p, "abc");
}

int
main()
{
    char *foo = NULL;

    fill_foo(&foo);
    printf("%s\n", foo);

    return 0;
}

(Fehlerbehandlung wurde Knappheit geopfert.)

So, wie du es hast, übergibst du deiner foo()-Funktion direkt den Pointer – by value. Wenn dein foo() beginnt, enthält test_pointer den Wert NULL. Und dann überschreibst du test_pointer halt mit einem neuen Wert, der von zmq_socket() zurückgegeben wird. Danach ist deine Funktion zuende und der Inhalt deines test_pointer verloren.

Du willst also eigentlich die Speicheradresse übergeben, an der der neu allokierte Pointer abgelegt werden kann (auch das passiert wieder by value). Deswegen rufe ich oben fill_foo(&foo) auf. Innerhalb meiner Punktion hat foo_p dann die Adresse von foo aus der main als Wert. Die beiden *foo_p dereferenzieren das, arbeiten also mit dem durch diese Adresse beschriebenen Speicher.

Ist das verständlich oder zu knapp formuliert?

Du hast wahrscheinlich ein Call-by-Reference wie bei C++ im Sinn: https://de.wikibooks.org/wiki/C%2B%2B-Programmierung/_Weitere_Grundelemente/_Prozeduren_und_Funktionen#call-by-reference Sowas geht in C nicht. Und ich habe jetzt einfach mal spekuliert, dass du mit C arbeiten willst. ☺

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6512

Wohnort: Hamburg

Warum gibst Du das Ergebnis nicht direkt zurück? Etwa so:

1
2
3
4
5
6
7
8
9
void * foo (void) {
     return zmq_socket(...);
}

int main(){
    ...
    ubergabe_pointer = foo();
    ...
}

Wenn es keine weiteren Randbedingungen gibt, finde ich das übersichtlicher.

Vain

Avatar von Vain

Anmeldungsdatum:
12. April 2008

Beiträge: 2510

Dakuan schrieb:

Warum gibst Du das Ergebnis nicht direkt zurück? [...] Wenn es keine weiteren Randbedingungen gibt, finde ich das übersichtlicher.

Oh, ja, gefällt mir auch viel besser. Gar nicht dran gedacht. 👍

Neral

Anmeldungsdatum:
3. Oktober 2007

Beiträge: 230

@Vain: In deinem Beispiel hast du ein Speicherleck erzeugt, weil zu deinem malloc nie free aufgerufen wird.

momooo

(Themenstarter)

Anmeldungsdatum:
11. Oktober 2017

Beiträge: 23

Vielen Dank Leute hat super funktioniert, jedoch bin ich mir bei einer Sache noch unsicher und zwar ist es ja so, dass wenn man in einer Funktion variablen deklariert, diese auf dem Stack landen und beim verlassen der Funktion gelöscht werden. Warum ist das hier nicht der Fall, also von Dakuans Lösung? Sollte mir die Funktion nicht NULL zurück geben?

Neral

Anmeldungsdatum:
3. Oktober 2007

Beiträge: 230

@momooo: Zuweisung ist in C eine bitweise Kopie. Argumentübergabe beim Funktionsaufruf und Rückgabe eines Wertes sind Zuweisungen.

Was nicht geht, ist einen Pointer auf etwas auf dem Stack zurückzugeben.

Beispiel:

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

uint8_t *foo(uint8_t *bar) {
    return bar + 1;
}

uint8_t baz(uint8_t fubar) {
    return fubar + 1;
}

int *undefined(void) {
    int value = 42;
    return &value;
}

foo und baz werden zu genau dem selben Maschinencode übersetzt werden (wenn eine Zwischenvariable für das Ergebnis der Addition eingeführt wird, bleibt der Maschinencode gleich...), das Verhalten von undefined ist nicht definiert, da ein Pointer auf eine Variable auf dem Stack zurückgegeben wird. (Dieser Code im Godbolt-Compiler-Explorer.) In diesem Fall gibt undefined einfach einen NULL-Pointer zurück (und der Compiler warnt), jedes andere Ergebnis wäre genau gleich „richtig“.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4694

Wohnort: Berlin

@Neral: Wie ähnlich sich der Maschinencode von foo() und baz() ist, dürfte vom Compiler und vom Zielsystem abhängen. Das ist ja selbst bei dem verlinkten Godbolt-Beispiel unterschiedlich weil die verwendete ABI bei Rückgabewerten Pointer im rax-Register und ganze Zahlen im eax-Register erwartet.

Hier noch mal das Beispiel in Godbolt für den x86-64 gcc:

1
2
3
4
5
6
7
8
9
foo:
  lea rax, [rdi+1]
  ret
baz:
  lea eax, [rdi+1]
  ret
undefined:
  lea rax, [rsp-4]
  ret

Mit Clang sehen foo() und bar() schon unterschiedlicher aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
foo:
  lea rax, [rdi + 1]
  ret
baz:
  inc dil
  mov eax, edi
  ret
undefined:
  lea rax, [rsp - 4]
  ret

Open Watcom C für 32-Bit (DOS):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
foo_:
    inc         eax 
    ret         
baz_:
    inc         al 
    ret         
undefined_:
    sub         esp,0x00000004 
    mov         dword ptr [esp],0x0000002a 
    mov         eax,esp 
    add         esp,0x00000004 
    ret         

Und noch was für kleinere/ältere Rechner: cc65 😎

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.proc	_foo: near
	jsr     pushax
	ldy     #$01
	lda     (sp),y
	tax
	dey
	lda     (sp),y
	clc
	adc     #$01
	bcc     L0001
	inx
L0001:	jmp     incsp2
.endproc

.proc	_baz: near
	jsr     pusha
	ldy     #$00
	lda     (sp),y
	clc
	adc     #$01
	ldx     #$00
	jmp     incsp1
.endproc

.proc	_undefined: near
	lda     #$2A
	jsr     pusha0
	lda     sp
	ldx     sp+1
	jmp     incsp2
.endproc

momooo

(Themenstarter)

Anmeldungsdatum:
11. Oktober 2017

Beiträge: 23

Vielen, vielen Dank Leute, ihr habt mich davor bewahrt globale Variablen zu verwenden. Dieses Forum ist wirklich eines der wenigen Foren, in welcher man aufgrund der gestellten Frage nicht gleich zur Sau gemacht also ein großes Lob und Dankeschön ☺

Lg

Vain

Avatar von Vain

Anmeldungsdatum:
12. April 2008

Beiträge: 2510

Neral schrieb:

@Vain: In deinem Beispiel hast du ein Speicherleck erzeugt, weil zu deinem malloc nie free aufgerufen wird.

Hatte ich bewusst weggelassen, weil das Programm direkt danach zuende ist und der Speicher damit automatisch wieder aufgeräumt wird. Ein „Leck“ wird das in der Situation ja nicht.

Aber wahrscheinlich ist es auch bei so einfachen Beispielen besser, konsequent free() hinzuschreiben, damit da nicht unterschwellig der Eindruck entsteht, es müsse generell nicht sein. Und, wie schon gesagt, muss der Rückgabewert von malloc() auch ausgewertet werden.

Ist so ein bisschen wie bei Quoting von Shell-Variablen in einfachen Beispielen. Wenn du weißt, was du tust, lässt du sie weg. Aber denkt der Leser dann, dass die Quotes nicht so wichtig sind? Oder ist es gerade ihr Fehlen, das ihn neugierig werden lässt? Wer weiß. Hatten wir ja schon genug Diskussion dazu. ☺

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6512

Wohnort: Hamburg

Warum ist das hier nicht der Fall, also von Dakuans Lösung?

Wenn ich mich richtig erinnere, gingen die Erfinder von C wohl davon aus, dass eine normale CPU mindestens einen Akkumulator hat, den man dann am Ende einer Funktion dazu benutzen kann, einen Wert an den Aufrufer zurück zu geben (könnte man auch über den Stack machen, was aber langsamer ist). Der Rückgabewert wird also einfach nur in den Akku kopiert und fertig. Der Aufrufer kann den Wert sofort weiter verwenden.

@Vain Ich denke, gerade wenn man es mit Einsteigern zu tun hat, sollte man das zumindest erwähnen. Aber ich lese immer wieder mal, das solche Funktionen "unerwünscht" sind. Trotzdem werden sie nicht aussterben. Bestes Beispiel

  scandir(...);

In C++ könnte man das Problem wohl mit Smart Pointern lösen, obwohl man da auch unangenehme Überraschungen erleben kann (hatte ich kürzlich 😢 ).

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4694

Wohnort: Berlin

@Duakan: Ich verstehe die Frage von momooo an der Stelle nicht wirklich, also weiss ich nicht ob Deine Antwort passt, aber die ist nicht so wirklich richtig IMHO. Ob es einen Akkumulator (oder irgendwelche Register) gibt oder nicht ist doch eigentlich egal, man kann C auch für ein Ziel ohne Register implementieren können. Wobei sich dann die Frage stellt ob man wirklich keine Register hat, oder aber gaaaanz viele, denn dann könnte man auch den kompletten RAM als viele Register ansehen. Ich hatte ja als letztes Beispiel in meinem letzten Beitrag Code für den Motorola 6510 gezeigt: der hat drei 8-Bit Register (A, X, und Y), also zu wenig um 32-Bit-Werte als Ergebnis über Register an den Aufrufer zurück zu geben.

Der cc65-Compiler verwendet deswegen für die Rückgabe von solchen Werten die Register A und X und zwei Speicherstellen im RAM. Und zwar in der „zero page“ — das sind beim 6510 die ersten 256 Bytes im RAM. Die haben ein eigene Adressierungsart bei der die höherwertigen 8 Bit der sonst 16-Bit Adressen weggelassen werden — die Befehle verwenden also weniger Speicher und werden schneller ausgeführt — und ein paar wichtige Adressierungsarten, wie indirekte Adressierung, funktionieren nur mit diesen Speicherstellen. Deswegen werden die auch oft als so etwas wie zusätzliche Register angesehen.

Da Du über den Stack geschrieben hast: Besondere Hardwareunterstüztung für einen Stack braucht ein Prozessor auch nicht für C. MIPS-Prozessoren haben beispielsweise 31 Register die an sich gleichwertig sind und keine speziellen PUSH/POP-Befehle. Das Register $29 als Stackzeiger verwendet wird ist reine Konvention — man könnte auch jedes andere Register dafür verwenden (ausser $0). C kann auch mehr als einen Stack verwenden. cc65 verwendet den Hardwarestack des 6510 zum Beispiel nur für die Rücksprungadresse. Argumente/lokale Variablen werden auf einem Softwarestack gespeichert. Mit den 256 Bytes vom Hardwarestack kommt man sonst nämlich nicht all zu weit.

Ob Argumente und lokale Variablen auf einem Stack landen, kann letztlich auch davon abhängen wie viele Prozessorregister zur Verfügung stehen und ob von den Werten Adressen genommen werden.

Funktionen wie scandir(), also mit Zeigern für ”Rückgabewerte”, wird man immer brauchen wenn eine Funktion mehr als einen Wert zurückgeben soll/muss. Also im Fall von scandir() die Anzahl der gefundenen Einträge/Fehlerwert und einen Zeiger auf das Array mit den gefundenen Einträgen. Das könnte man umgehen in dem man eine Datenstruktur (oder einen Zeiger auf eine solche) zurück gibt, die beide Informationen enthält. Wie Smart Pointer das lösen können sehe ich jetzt nicht auf Anhieb‽

Mit welcher Begründung ist das denn ”unerwünscht”? Es gibt ja sogar Sprachen bei denen solche Argumente mit einem Schlüsselwort (z.B. out) gekennzeichnet/deklariert werden, oder mit & als Referenzen in C++. In QBasic betrifft das grundsätzlich alle Argumente. Das fand ich neulich mal sehr, äh, überraschend. ☺

1
2
3
4
5
6
7
8
SUB Foo (i AS INTEGER)
  i = i + 1
END SUB

DIM a AS INTEGER
a = 42
Foo a
PRINT a  ' Prints 43!

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6512

Wohnort: Hamburg

Ob es einen Akkumulator (oder irgendwelche Register) gibt oder nicht ist doch eigentlich egal, man kann C auch für ein Ziel ohne Register implementieren können.

Das war nur als anschauliches Beispiel gedacht und basiert auf früheren Erfahrungen mit Z80 und MC68000. Da passte das noch.

Die Erwähnung von Smart Pointern bezog sich auf des entdeckte Speicherleck und nicht auf scandir(). Da hätte ich meine Gedanken wohl sorgfältiger sortieren sollen.

Mit welcher Begründung ist das denn ”unerwünscht”?

Das kann ich jetzt auch nicht sagen. Ich habe immer wieder mal Aufzählungen gelesen was man als Programmierer alles nicht machen sollte und habe mich dabei recht oft wieder erkannt. Alles was mir dazu einfällt ist eben, das man leicht free() vergessen kann.

Aber da ich auch keine einfache Alternative kenne, habe ich beschlossen mich darüber hinweg zu setzen. Mein Code bekommt ja niemand anderes zu sehen.

Neral

Anmeldungsdatum:
3. Oktober 2007

Beiträge: 230

@Marc_BlackJack_Rintsch: Natürlich hast du recht, dass der generierte Maschinencode vom Zielsystem abhängt und dass er auch auf x86_64 nicht genau gleich aussieht. Ich wollte allerdings hauptsächlich darauf hinweisen, dass man einen lokalen Pointer genauso wie einen lokalen int (oder jeden anderen Wert) zurückgeben kann.

Wie hast du den GCC dazu gebracht, in undefined ein lea rax, [rsp-4] zu generieren? Bei mir kommt ein xor eax, eax raus.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4694

Wohnort: Berlin

@Neral: Man muss nur einen GCC auswählen der alt genug ist. Irgendeinen 4.irgendwas. Ich hatte nicht auf die Sortierung geachtet und den nach „trunk“ aus der Liste ausgewählt.

Eine Variante mit grossem Unterschied zwischen dem generierten Code habe ich noch: Watcom C für 16-Bit mit dem „huge“-Speichermodell, also Code und Daten sind FAR-Zeiger und einzelne Datenobjekte dürfen grösser als 64KiB sein:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
foo_:
    push        bx 
    push        cx 
    mov         bx,0x0001 
    xor         cx,cx 
    call        __PIA 
    pop         cx 
    pop         bx 
    retf        
baz_:
    inc         al 
    retf        

Hach ja, da fühlt man sich alt und kann den jungen vorjammern wie schlimm das früher alles war. Wir hatten ja nichts. Nicht mal ordentlich viel, einfach linear addressierbaren Speicher. Nicht nur das die heute mit so komischen Hochsprachen arbeiten — wenn sie denn mal Assembler schreiben, dann müssen sie sich nicht einmal mehr mit Segmentregistern und NEAR- und FAR-Zeigern, oder der Speichergrösseneinheit „paragraph“ auseinandersetzen. ☺

Antworten |