Was passiert genau, wenn ich einen Point mit NULL initialisiere?
-
SeppJ schrieb:
asc schrieb:
Diese Aussage sehe ich als gefährlich an, da ich es durchaus von einigen Debuggern kenne das man uninitialisierte Zeiger im Debugmodus eben nicht bemerkt, da sie vom Debugger mit 0 initialisiert wurden, im Release aber ein Zufallswert in diesen steht.
Und da sieht man wieder, warum es falsch ist, Zeiger grundsätzlich mit 0 zu initialisieren.
Ein Zufallswert ist aber genauso unsinnig.
Ich initialisiere Zeiger (wenn ich normale verwende) grundsätzlich mit 0, wenn sie noch keinen Wert zugewiesen haben, ansonsten mit einer gültigen Speicheradresse. Ein anderer Initialisierungswert macht imho nur dann Sinn, wenn es einen eindeutigen Wert für "Uninitialisiert" gäbe.
-
asc schrieb:
Ein Zufallswert ist aber genauso unsinnig.
Ich initialisiere Zeiger (wenn ich normale verwende) grundsätzlich mit 0, wenn sie noch keinen Wert zugewiesen haben, ansonsten mit einer gültigen Speicheradresse. Ein anderer Initialisierungswert macht imho nur dann Sinn, wenn es einen eindeutigen Wert für "Uninitialisiert" gäbe.
Warum das? Andere Variablen, welche kein Zeigertyp sind, initialisiert man ja auch nicht zwangsläufig sofort mit einem sinnvollen Wert.
Ohne Zweisung bzw. new macht ein Zeiger wenig Sinn. Und danach zeigt er entweder auf etwas sinnvolles, oder auf 0 (oder es wird eine Exception geschmissen, wenn man es sagt). Und dann sollte man eh prüfen, ob alles gut gegangen ist.
-
Tachyon schrieb:
Warum das? Andere Variablen, welche kein Zeigertyp sind, initialisiert man ja auch nicht zwangsläufig sofort mit einem sinnvollen Wert.
Ich versuche Variablen immer erst zu deklarieren, wenn ich sie initialisieren kann, und in der Initialisierungsliste führe ich schon alleine um keine Variable zufällig zu übersehen auch alle auf. In sofern sehe ich das anders.
Tachyon schrieb:
Ohne Zweisung bzw. new macht ein Zeiger wenig Sinn. Und danach zeigt er entweder auf etwas sinnvolles, oder auf 0 (oder es wird eine Exception geschmissen, wenn man es sagt). Und dann sollte man eh prüfen, ob alles gut gegangen ist.
Dieser Ansatz kann aber sehr schnell nach hinten gehen, vor allem wenn man Programmierer im Team hat, die Variablen ganz weit entfernt von der Verwendung deklarieren. Ich habe lieber ein sauber definiertes Verhalten (selbst eine AccessViolation mit Zugriff auf Adresse 0 halte ich für sinnvoller als Zufallswerte die weiß was bedeuten können, und die im Laufenden Betrieb schlecht reproduzierbare Fehler nach sich ziehen).
Ich riskiere lieber einen zu 100% reproduzierbaren Absturz als einen, der verdammt schwierig zu finden ist, und ggf. auch von der QS übersehen wird [der reproduzierbare Absturz wird weit leichter gefunden].
-
SeppJ schrieb:
@Paul Müller: Das klingt alles so, als würdest du so mit Pointern programmieren, dass man darauf tatsächlich aufpassen müsste, d.h. einfach wild drauflos.
Das hat mit meinen Programmierkünsten erstmal wenig zu tun. Da gibt es halt Code, den hat irgendjemand anderes mal geschrieben und da musst du jetzt was ran/rein basteln. Natürlich ist da nicht die Lebenszeit jedes einzelnen Pointers dokumentiert. Aber dafür gibt es umso mehr Pointer und dirty Tricks, die man mit Pointern machen kann.
SeppJ schrieb:
Idealerweise programmiert man so, dass solche Probleme gar nicht erst auftreten können.
Das ist meist schwierig, wenn es um zeitkritische Aufgaben geht.
SeppJ schrieb:
allgemein: delete prüft ja ohnehin irgendwie, ob die Freigabe gültig ist.
Im einfachsten Fall würdest du dann eine Exception bekommen, diese müsste du aber wieder gesondert behandeln. Und wie schon angemerkt, was heißt gültig, ein Speicherbereich kann auch wieder vergeben werden.
SeppJ schrieb:
Da ist es doch kein Problem, die 0 gesondert zu behandeln, ohne dass es merkliche Laufzeitverluste gibt.
Es geht ja auch mehr darum definieren zu können, ob ein Zeiger gültig ist oder nicht. Von new und delete soll man ja auch die Finger lassen soweit es möglich ist.
-
Paul Müller schrieb:
...Von new und delete soll man ja auch die Finger lassen soweit es möglich ist...
Das würde ich so nicht stehen lassen, auch wenn man sich fragen sollte ob man sich selbst um die Speicherverwaltung kümmern muss. Stackvariablen sind Heapvariablen zwar häufig vorzuziehen, nur ist der Stack zumeist begrenzt, und manches lässt sich auf dem Heap besser regeln.
-
Paul Müller schrieb:
...
Ich würde sogar soweit gehen und delete einfach den Zeiger NULL setzen lassen. Aber das wird wahrscheinlich nicht den Weg in den Standard finden. Diese eine Anweisung tut keinem weh, im Verhältnis zum restlichen delete, aber würde soviel bringen.Bjarne Stroustrup schrieb:
C++ explicitly allows an implementation of delete to zero out an lvalue operand, and I had hoped that implementations would do that, but that idea doesn't seem to have become popular with implementers.
If you consider zeroing out pointers important, consider using a destroy function:
template<class T> inline void destroy(T*& p) { delete p; p = 0; }
Man beachte aber auch http://www2.research.att.com/~bs/bs_faq2.html#delete-zero
-
asc schrieb:
Diese Aussage sehe ich als gefährlich an, da ich es durchaus von einigen Debuggern kenne das man uninitialisierte Zeiger im Debugmodus eben nicht bemerkt, da sie vom Debugger mit 0 initialisiert wurden, im Release aber ein Zufallswert in diesen steht.
Ist ne ziemliche schwäche des Compilers dann.
Der VC++ setzt zB uninitialisierte Zeiger auf 0xCDCDCDCDUnd jeder Zugriff auf diese Adresse ist sofort ein breakpoint.
-
asc schrieb:
Das würde ich so nicht stehen lassen, auch wenn man sich fragen sollte ob man sich selbst um die Speicherverwaltung kümmern muss.
Darauf wollte ich jetzt gar nicht hinaus. Mir ging es um die Zeit, die ein new/delete verbrät. Deswegen sollten man damit lieber sparsam umgehen. Objekte verschlimmern diese ganze Angelegenheit auch nur.
Das hängt aber auch stark von der Zielplattform ab. Auf einem normalen Desktop-Computer braucht man sich in der Regel keine Gedanken darum machen.
-
Paul Müller schrieb:
...gefährliches Halbwissen...
Tut mir leid, aber fast alles was du in deinen letzten beiden Beiträgen geschrieben hast ist Quatsch.
-
@SeppJ
Was den genau? Ich kann im letzten Beitrag von mir keinen Unsinn entdecken. Maximal eine Pauschalisierung bzgl. ob man auf Desktops ressourcensparend programmieren sollten. Wenn ich dich aber richtig verstehe, würdest du die Aussage sogar ausweiten, anstatt einschränken wollen?
-
Paul Müller schrieb:
@SeppJ
Was den genau? Ich kann im letzten Beitrag von mir keinen Unsinn entdecken. Maximal eine Pauschalisierung bzgl. ob man auf Desktops ressourcensparend programmieren sollten. Wenn ich dich aber richtig verstehe, würdest du die Aussage sogar ausweiten, anstatt einschränken wollen?Ok, hier eine detaillierte Analyse:
Mir ging es um die Zeit, die ein new/delete verbrät. Deswegen sollten man damit lieber sparsam umgehen.
Und was soll man stattdessen nehmen? Entweder braucht man Heapobjekte oder eben nicht. Grundlos new/delete benutzen meistens nur Leute die von Java kommen, weil sie es nicht anders kennen.
Objekte verschlimmern diese ganze Angelegenheit auch nur.
Unsinn. Abstraktion kostet keine Laufzeit.
SeppJ schrieb:
Idealerweise programmiert man so, dass solche Probleme gar nicht erst auftreten können.
Das ist meist schwierig, wenn es um zeitkritische Aufgaben geht.
Unsinn. Abstraktion kostet keine Laufzeit.
SeppJ schrieb:
allgemein: delete prüft ja ohnehin irgendwie, ob die Freigabe gültig ist.
Im einfachsten Fall würdest du dann eine Exception bekommen, diese müsste du aber wieder gesondert behandeln.
Was willst du mir damit sagen? Ich schildere nur den Sachverhalt. Es ging darum, ob das delete dadurch langsamer wird, weil es die 0 gesondert behandelt. Das wird es nicht wesentlich, weil beim delete sowieso irgendwelche Prüfungen durchgeführt werden.
Und wie schon angemerkt, was heißt gültig, ein Speicherbereich kann auch wieder vergeben werden.
Gültig heißt, dass es vorher ein passendes new gab. Speicher wird nicht wieder vergeben, bevor er nicht freigegeben wurde. Wenn du über mehrere Pointer auf den gleichen Speicherbereich zugreifst (so dass er zwischendurch freigegeben und wieder zugewiesen werden kann, ohne dass ein anderer Pointer es merkt), sind wir wieder im Bereich unsauberer Programmierung.
Von new und delete soll man ja auch die Finger lassen soweit es möglich ist.
Da haste Recht. Aber ich glaube die Gründe sind dir nicht so ganz klar.
-
SeppJ schrieb:
Und was soll man stattdessen nehmen? Entweder braucht man Heapobjekte oder eben nicht.
Man kann die Objekte auch einfach recyceln. Das man irgendwann mal ein Objekt erstellen muss steht dabei nicht zur Debatte.
SeppJ schrieb:
Unsinn. Abstraktion kostet keine Laufzeit.
Du willst mir nicht ernsthaft erzählen, dass ein ctor/dtor Aufruf genauso viel Laufzeit benötigt wie kein Aufruf?
SeppJ schrieb:
Unsinn. Abstraktion kostet keine Laufzeit.
Hier das gleiche. Du meinst nicht ernsthaft etwas zu machen geht genauso schnell, wie etwas nicht zu machen?
SeppJ schrieb:
Was willst du mir damit sagen? Ich schildere nur den Sachverhalt. Es ging darum, ob das delete dadurch langsamer wird, weil es die 0 gesondert behandelt. Das wird es nicht wesentlich, weil beim delete sowieso irgendwelche Prüfungen durchgeführt werden.
Meinetwegen werden Prüfungen durchgeführt. Diese belaufen sich aber auf NULL, der Trivialfall. Und ob der Zeiger mit new allokiert wurde. Andernfalls gibts eine Exception, die du fangen müsstest und irgendwas damit anstellen oder dein Programm kracht komplett weg.
Die mühselige Exception kann ich mir sparen, indem ich einfach den Pointer NULL setze. Und das hab ich zwar schon erwähnt aber ein delete zuviel tut nicht weh, nur ein delete zu wenig. Und wenn es nicht weh tut, ist es mir auch egal, ob man es als unsauber ansehen kann. Der Link von manni66 zeigt zwar, dass das nicht immer funktioniert, aber das kannst du ja in deinem Design berücksichtigen.SeppJ schrieb:
Gültig heißt, dass es vorher ein passendes new gab. Speicher wird nicht wieder vergeben, bevor er nicht freigegeben wurde.
Das hat auch niemand bestritten.
SeppJ schrieb:
Wenn du über mehrere Pointer auf den gleichen Speicherbereich zugreifst (so dass er zwischendurch freigegeben und wieder zugewiesen werden kann, ohne dass ein anderer Pointer es merkt), sind wir wieder im Bereich unsauberer Programmierung.
Siehe meine Anmerkung zu manni66 seinem Link.
SeppJ schrieb:
Da haste Recht. Aber ich glaube die Gründe sind dir nicht so ganz klar.
Ich denke schon. Ich finde es nur etwas merkwürdig das du bestreiten willst, dass ein Funktionsaufruf Laufzeit kostet. Ich denke die Bedeutung von inline-Funktionen ist dir vertraut?
-
Paul Müller schrieb:
SeppJ schrieb:
Unsinn. Abstraktion kostet keine Laufzeit.
Du willst mir nicht ernsthaft erzählen, dass ein ctor/dtor Aufruf genauso viel Laufzeit benötigt wie kein Aufruf?
Du willst mir nicht ernsthaft erzählen, dass da tatsächlich eine Funktion aufgerufen wird.
Die mühselige Exception kann ich mir sparen, indem ich einfach den Pointer NULL setze.
Die Exception und alles andere kannst du dir gleich sparen, indem du vernünftig programmierst und nicht irgendwelche Zeiger löscht, von denen du nicht einmal weißt, wohin sie zeigen.
Und das hab ich zwar schon erwähnt aber ein delete zuviel tut nicht weh, nur ein delete zu wenig.
Wenn du ein delete zu viel machst, dann hat dein Programm einen Fehler. An anderer Stelle. Und du merkst es noch nicht einmal, wenn du immer blind alles nullst.
SeppJ schrieb:
Da haste Recht. Aber ich glaube die Gründe sind dir nicht so ganz klar.
Ich denke schon. Ich finde es nur etwas merkwürdig das du bestreiten willst, dass ein Funktionsaufruf Laufzeit kostet. Ich denke die Bedeutung von inline-Funktionen ist dir vertraut?[/quote]Besser als dir offensichtlich.
-
SeppJ schrieb:
Du willst mir nicht ernsthaft erzählen, dass da tatsächlich eine Funktion aufgerufen wird.
Ich hoffe das soll ein Scherz sein. Selbst ein Anfänger würde nicht bestreiten wollen, dass ein geschriebener ctor eine Funktion ist. Aber selbst ein impliziter ctor oder einer mit nur einer Initialisierungsliste braucht Laufzeit. Auch wenn das lächerlich wenig ist, meinst du der Speicher füllt sich von allein?
SeppJ schrieb:
Die Exception und alles andere kannst du dir gleich sparen, indem du vernünftig programmierst und nicht irgendwelche Zeiger löscht,
Ich lösche nicht irgendwelche Zeiger. Das löschen ist auch wie schon mal angemerkt völlig uninteressant. Viel wichtiger ist es keine falschen Zeiger zu dereferenzieren. Und dass kann ich mit NULL auch viel besser machen, als wenn ich mir das Programm um die Ohren hauen lasse.
SeppJ schrieb:
von denen du nicht einmal weißt, wohin sie zeigen.
Was ja nur der Fall wäre, wenn ich sie nicht nullen würde. Siehe das Beispiel, wo der Zeiger oder besser die Adresse, schon wieder für eine Allokation verwendet wurde.
SeppJ schrieb:
Wenn du ein delete zu viel machst, dann hat dein Programm einen Fehler. An anderer Stelle. Und du merkst es noch nicht einmal, wenn du immer blind alles nullst.
Ich nulle nicht blind alles, sondern nur Zeiger die ich gelöscht habe. Somit merke ich dann in dem Fall schmerzlos, dass ich das Objekt zu früh gelöscht habe. Wenn ich einfach einen gültigen Zeiger NULL setze und damit die Verbindung verliere, um das Objekt zu löschen, dies wäre fatal, richtig.
-
Paul Müller schrieb:
SeppJ schrieb:
Du willst mir nicht ernsthaft erzählen, dass da tatsächlich eine Funktion aufgerufen wird.
Ich hoffe das soll ein Scherz sein. Selbst ein Anfänger würde nicht bestreiten wollen, dass ein geschriebener ctor eine Funktion ist. Aber selbst ein impliziter ctor oder einer mit nur einer Initialisierungsliste braucht Laufzeit. Auch wenn das lächerlich wenig ist, meinst du der Speicher füllt sich von allein?
Und der Anfänger läge damit falsch. Du auch. Seit wann ist Speicherfüllen ein Funktionsaufruf?
-
SeppJ schrieb:
Paul Müller schrieb:
SeppJ schrieb:
Du willst mir nicht ernsthaft erzählen, dass da tatsächlich eine Funktion aufgerufen wird.
Ich hoffe das soll ein Scherz sein. Selbst ein Anfänger würde nicht bestreiten wollen, dass ein geschriebener ctor eine Funktion ist. Aber selbst ein impliziter ctor oder einer mit nur einer Initialisierungsliste braucht Laufzeit. Auch wenn das lächerlich wenig ist, meinst du der Speicher füllt sich von allein?
Und der Anfänger läge damit falsch. Du auch. Seit wann ist Speicherfüllen ein Funktionsaufruf?
Also ihr sprecht da irgendwie über 2 verschiedene Sachen.
Initialisierung von Variablen kostet Zeit gegenüber dem Nichtinitialisieren. Ich glaube da sind wir uns einig.
Und ob der Konstruktor ein Funktionsaufruf ist oder nicht ist völlig plattformabhängig und wahrscheinlich auch davon was der Konstruktor im Endeffekt tut. Wenn es nur einen leeren Defaulkonstruktor gibt wird da wahrscheinlich nichts gemacht, sind aber mehrere verschiedene Konstruktoren vorhanden werden das sehr wahrscheinlich Funktionsaufrufe sein. Man müsste das halt mal überprüfen und ein wenig asm anschauen, aber ich kann mir das kaum anders vorstellen.
-
SeppJ hat vollkommen recht.
Denn wenn man im Ctor 3h lang initialisiert, so muss man das ohne Ctor ja auch tun, sonst wäre der Code nicht äquivalent. Wenn ich 500 Variablen füllen muss, muss ich das ob ich Ctors habe oder nicht.
Abstraktion kostet keine Performance in C++.
-
Ich glaube, es geht dem Paul um den Overhead für Funktionsaufrufe. Da sollte man wissen, dass sich Funktionsaufrufe inlinen lassen...
-
Damit du siehst, dass ich dir nicht bloß was erzähle:
Folgendes Programm:int main() { int data; cin>>data; cout<<data<<endl; }
gegen dieses:
class int_object { private: int data; public: friend ostream& operator<<(ostream &out, const int_object& in) { out<<in.data; return out; } friend istream& operator>>(istream &in, int_object& out) { in>>out.data; return in; } }; int main() { int_object data; cin>>data; cout<<data<<endl; }
Eines operiert direkt auf nativen Datentypen. Das andere operiert auf einem Klassenobjekt. Resultierender Maschinencode?
Dies für die erste Variante (gcc, -O3), relevanter Teil:.LFB1002: .cfi_startproc .cfi_personality 0x3,__gxx_personality_v0 pushq %rbp .cfi_def_cfa_offset 16 movl $_ZSt3cin, %edi pushq %rbx .cfi_def_cfa_offset 24 subq $24, %rsp .cfi_def_cfa_offset 48 leaq 12(%rsp), %rsi .cfi_offset 3, -24 .cfi_offset 6, -16 call _ZNSirsERi movl 12(%rsp), %esi movl $_ZSt4cout, %edi call _ZNSolsEi movq %rax, %rbx movq (%rax), %rax movq -24(%rax), %rax movq 240(%rbx,%rax), %rbp testq %rbp, %rbp je .L9 cmpb $0, 56(%rbp) je .L5 movzbl 67(%rbp), %eax .L6: movq %rbx, %rdi movsbl %al,%esi call _ZNSo3putEc movq %rax, %rdi call _ZNSo5flushEv addq $24, %rsp xorl %eax, %eax popq %rbx popq %rbp ret .p2align 4,,10 .p2align 3 .L5: movq %rbp, %rdi call _ZNKSt5ctypeIcE13_M_widen_initEv movq (%rbp), %rax movl $10, %esi movq %rbp, %rdi call *48(%rax) jmp .L6 .L9: .p2align 4,,5 call _ZSt16__throw_bad_castv .cfi_endproc
Dies für die zweite Variante:
.globl main .type main, @function main: .LFB999: .cfi_startproc .cfi_personality 0x3,__gxx_personality_v0 pushq %rbx .cfi_def_cfa_offset 16 movl $_ZSt3cin, %edi subq $16, %rsp .cfi_def_cfa_offset 32 movq %rsp, %rsi .cfi_offset 3, -16 call _ZNSirsERi movl (%rsp), %esi movl $_ZSt4cout, %edi call _ZNSolsEi movq _ZSt4cout(%rip), %rax movq -24(%rax), %rax movq _ZSt4cout+240(%rax), %rbx testq %rbx, %rbx je .L9 cmpb $0, 56(%rbx) je .L5 movzbl 67(%rbx), %eax .L6: movsbl %al,%esi movl $_ZSt4cout, %edi call _ZNSo3putEc movq %rax, %rdi call _ZNSo5flushEv xorl %eax, %eax addq $16, %rsp popq %rbx ret .p2align 4,,10 .p2align 3 .L5: movq %rbx, %rdi call _ZNKSt5ctypeIcE13_M_widen_initEv movq (%rbx), %rax movl $10, %esi movq %rbx, %rdi call *48(%rax) jmp .L6 .L9: .p2align 4,,5 call _ZSt16__throw_bad_castv .cfi_endproc
Unterschied der beiden? Ein paar andere Adressen und Registernamen. Und ein leaq wurde durch ein movq ersetzt.
-
SeppJ, um 100%ig zu überzeugen müsstest du beweisen, dass alle im Umlauf befindlichen C++-Compiler effektlose Funktionsaufrufe unter allen Umständen wegoptimieren.
Edit: Wobei ich mich da einfach drauf verlasse und mir lieber über andere Dinge den Kopf zerbreche...^^
Edit %2: Und in diesem fall auch, dass sie immer inlinen, wenn das ergebnis kürzer oder gleich dem Funktionsaufruf wäre.