Hallo Leute,
Programmiert jemand von euch in c++ (objektorientiert) auf den Atmel
8-Bit Controllern ?
Wenn ja, wie einfach nur zum Beispiel eine Klasse für twi eine für den
usart etc mit entsprechenden Feldern, Methoden Konstruktor und
Destruktor ?
Wenn jemand mit dem Objektorientierten Ansatz arbeitet wäre es nett,
wenn kurz erläutert werden würde, auf was man dabei achten muss.
Grüße
Sven
Sven schrieb:> Programmiert jemand von euch in c++ (objektorientiert) auf den Atmel> 8-Bit Controllern ?
Ja.
Allerdings ist das ein deutlich eingeschränktes C++. Dynamische
Speicherverwaltung, STL und Konsorten sind eher unangebracht.
Funktional nützlich ist das beispielsweise, wenn man Sensoren
verschiedener Typen einsetzt, wie DS1820 und LM335, die sich über
Ableitung von Klassen elegant mit einem einheitlichen Interface versehen
lassen.
Templates sind in bestimmten Fällen auch recht nützlich, beispielsweise
zur Spezifizierung von Parametern von Treiber für I/O-Module, wie etwa
Puffergrössen.
Das Thema kam hier schon mal auf; vielleicht hilft die Suchfunktion.
Z.B. hier: Beitrag "Festkommazahlen in Würde"
Der Grundtenor ist:
- ja, es geht
- C++ muß nicht langsamer sein oder mehr Code produzieren, kann es aber
- man muß sich zurückhalten
- die STL kann man sich komplett abschminken
- Mit templates kann man mit wenigen Zeilen viel Code erzeugen
lassen; man sollte wissen was man tut.
- Mit Ableitungen und virtuellen Methoden ist ebenfalls nicht zu
spaßen. Im Einzelfall sinnvoll, aber man muß auf generierte
Codegröße und Speicherverbrauch achten.
- Überladen von Operatoren kann sehr hilfreich sein und den
Quelltext enorm verbessern. Aber auch hier muß man sich
bewusst machen, was dahinter steht hinsichtlich Code und
Laufzeit.
Zum Schreiben auf UART habe ich auch mal eine Klasse gebaut, weil
ich es leid war, jedesmal wieder zu überlegen, was auf dem heutigen
AVR anders ist als beim gestrigen hinsichtlich Registernamen etc..
Ebenso in C++ ist eine Text-LCD-Klasse von mir, die ich von der
Benutzung her wesentlich angenehmer finde als das hier übliche
Vorgehen mit einer Headerdatei, in der Pins eingestellt werden
müssen, bevor man sie verwenden kann - auch wenn letztlich
intern nichts anderes passiert natürlich.
Ein #include "..." und Methoden aufrufen finde ich angemessener als
in jedem Quelltext wieder mit low leve-Kram zu fummeln.
Das Gefummel gehört einmal in eine Headerdatei, und gut.
Sven schrieb:> Hallo Leute,>> Programmiert jemand von euch in c++ (objektorientiert) auf den Atmel> 8-Bit Controllern ?> Wenn ja, wie einfach nur zum Beispiel eine Klasse für twi eine für den> usart etc mit entsprechenden Feldern, Methoden Konstruktor und> Destruktor ?> Wenn jemand mit dem Objektorientierten Ansatz arbeitet wäre es nett,> wenn kurz erläutert werden würde, auf was man dabei achten muss.>> Grüße>> Sven
Man kann ganz gut C++ Programmieren auf AVRs. Der Erfolg der Arduino
-Plattform beweist das es geht. Auch wenn Puristen laut aufschreien.
Wie Klaus schon gesagt hat: Man sollte beim Vererbungen und virtuellen
Funktionen sehr vorsichtig sein, den diese erzeugen einen riesigen
Overhead wie alles was an Objektorientierung nicht zur Compilezeit
aufgelöste werden kann. Templates und Operatorüberladung können einen
Quelltext übersichtlicher machen, man muss halt aufpassen und wissen was
der Compiler draus macht.
Stefan H. schrieb:> Wie Klaus schon gesagt hat: Man sollte beim Vererbungen und virtuellen> Funktionen sehr vorsichtig sein, den diese erzeugen einen riesigen> Overhead
Riesig? Overhead ja, aber riesig ist der nicht. Braucht halt auf den
AVRs ein bisschen mehr RAM, weil die als Harvard-Kisten die VFTs nicht
ins ROM kriegen. Das kann insbesondere auf den Tinys spürbar werden. Von
Code her ist es nicht so arg.
Das hat jetzt nichts mit C oder C++ zu tun. Ich wüsste nur nicht, wieso
man so viel Code für unterschiedliche Displays usw da rein packen muss.
Zur Laufzeit hat man selten die Notwendigkeit, ein anderes Display zu
verwenden.
Meine USART Routine hat nichtmal eine Funktion, die Baudrate zu ändern.
Das geschieht alles zur Compilezeit.
In meiner C++-LCD-Variante wird es in aller Regel so sein, daß man eben
auch nur eine LCD-Variante hat, der Compiler konstante Werte als solche
erkennt und vom ganzen Code eh alles wegoptimiert, was nicht genutzt
wird.
Die Methoden sind alle inline, und die ganzen Fallunterscheidungen
hängen nur von Konstanten ab.
Deshalb sieht es im Quelltext beeindruckend aus, aber nach dem
Kompilieren sollte nicht mehr übrig sein als bei einer hingefummelten
C-Version für einen konkreten Fall.
Was in meiner C++-Version höchstens mehr Code erzeugt als in der hier
üblichen C-Version ist die beliebige Verteilung der Anschlüsse auf Ports
und Pins. Ene handgeschneiderte Version, bei der alle Pins in
codefreundlicher Reihenfolge (D4-D7 auf Pin 0-3) ist sicher etwas
kleiner.
Aber bei ähnlichem Funktionsumfang ist C++ hier im Endeffekt nicht oder
nur unwesentlich fetter.
Gerade deshalb finde ich es ein nettes Beispiel für dieses Thema.
Das sollte jetzt keine Kritik an deiner C++ Implementierung sein. Ich
hab es mal compiliert wie es war und bekam fast 13kByte Code raus, wenn
ich mich nicht irre.
Die C Lib, die ich mal geschrieben habe, kann vielleicht nicht ganz so
viel (z.B. printf), aber dafür würde so ein Programm nur 2-3kByte
brauchen.
Ich meine, was soll der Compiler da groß wegoptimieren? switch(LCD_Type)
dürfte wohl komplett im Flash landen. Oder ist der Compiler so
intelligent, dass er erkennt, welche Cases nicht benutzt werden?
Markus B. schrieb:> Das sollte jetzt keine Kritik an deiner C++ Implementierung sein. Ich> hab es mal compiliert wie es war und bekam fast 13kByte Code raus, wenn> ich mich nicht irre.
Das ist heftig.
Meine LCD-Lib benötigt 290 Byte:
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=102296
LCDs mit 2 Enable sind nicht drin, das läßt sich aber leicht nachrüsten.
Etwas lästig ist, daß die Pindefinitionen immer zweimal gemacht werden
müssen, je einmal für Ausgangs- und Richtungsbit.
Peter
Markus B. schrieb:> Das sollte jetzt keine Kritik an deiner C++ Implementierung sein. Ich> hab es mal compiliert wie es war und bekam fast 13kByte Code raus, wenn> ich mich nicht irre.
Das hängt wohl davon ab, wie man damit umgeht. Wenn man daraus eine
globale Variable macht, dann hat man verloren, denn dann wird der ganze
Code minutiös eingebaut. Er hatte das wohl so vorgesehen, dass man in
jeder Routine diese Variable lokal definiert, wie in seinem
Beispielcode. Dann fieselt der Compiler durch Datenflussanalyse die
ganzen Konstanten raus und optimiert es.
Ich würde das per Template realisieren, mit den Invarianten als
Parameter des Templates. An passende Stelle kommt dann ein typedef mit
den entsprechenden Werten als Argumente und damit kann man dann
arbeiten. Sämtliche Pseudo-Variablen, die hier doch nur Konstanten
durchreichen, entfallen dann.
A. K. schrieb:> Das hängt wohl davon ab, wie man damit umgeht.
Sicher. Aber ich bin generell der Anhänger der "Compile Time
Definitionen". Also schon vor dem compilieren festlegen, um welches
Display es sich handelt oder welche Baudrate ein Comport haben soll.
Der Compiler kann noch so gut sein, aber den zusätzlichen Code kann er
auch nicht wegoptimieren.
Nur mal ein Beispiel. Den Code macht nicht viel mehr als die Ausgabe von
zwei Texten und erzeugt folgendes:
1
Size after:
2
AVR_LCD44780.elf :
3
section size addr
4
.text 12976 0
5
.data 20 8388704
6
.stab 10944 0
7
.stabstr 3405 0
8
Total 27345
Mein Beispiel hat noch USART mit drin und ließt Text aus dem Flash und
erzeugt das
Markus B. schrieb:> Der Compiler kann noch so gut sein, aber den zusätzlichen Code kann er> auch nicht wegoptimieren.
Doch, im Prinzip schon.
Mit einer Ausnahme können sämliche switch(LCD_Type)/case Zweige als
toter Code komplett rausfliegen, wenn der Compiler LCD_Type als
Konstante ansieht.
Indirekte Portzugriffe werden zu direkten Zugriffen, wenn der Pointer
konstant ist. Dynamische Shifts werden zu konstanten Masken, usw...
Nicht verwendete Funktionen erzeugen keinerlei Code. Allerdings sehe ich
bei ihm das Risiko, dass durch das Inlining der gleiche Code vielfach
repliziert wird.
Was dann noch bleibt ist der generische Umgang mit den einzelnen
Portpins. Andere Libs gehen gern mal davon aus, dass die 4 Datenbits
nebeneinander auf dem gleichen Port stehen.
Der Trick besteht darin, das so hinzudeichseln, dass die ganzen
Instanzvariablen, die durch die Konstruktorparameter initialisiert
werden, vom Compiler auch als Konstanten angesehen werden. Da könnte
auch helfen, die Variable "lcd" als Konstante zu definieren.
Mit Templates sieht der Quellcode ziemlich ähnlich aus, man hängt aber
nicht so an der Optimierung, weil der Umweg über die Instanzvariablen
entfällt. Der Zwang zum Inlining entfällt hier, so dass auch die
Replikation von Code nicht auftritt.
Markus B. schrieb:> Ich> hab es mal compiliert wie es war und bekam fast 13kByte Code raus, wenn> ich mich nicht irre.>> Die C Lib, die ich mal geschrieben habe, kann vielleicht nicht ganz so> viel (z.B. printf), aber dafür würde so ein Programm nur 2-3kByte> brauchen.
Deshalb hatte ich oben schon mal gefragt, was du eigentlich vergleichst.
Wenn du die C++-Version mit printf nimmst, aber das nicht in der eigenen
Version nutzt, kannst man das nicht vergleichen.
Mit printf bist bei deiner Version sicher auch gleich über 2 kB rübe,
wenn du es in der C++-Version nicht nimmst, wird es kleiner, da ja wie
gesagt alles inline ist.
Es ist sinnlos irgendwas mit irgendwas zu vergleichen, und daraus
Schlüsse zu ziehen.
Nur Zahlen in den Raum zu werfen ist Zeitverschwendung.
A. K. schrieb:> Mit Templates sieht der Quellcode ziemlich ähnlich aus, man hängt aber> nicht so an der Optimierung, weil der Umweg über die Instanzvariablen> entfällt. Der Zwang zum Inlining entfällt hier, so dass auch die> Replikation von Code nicht auftritt.
Dafür hast bei z.B. bei zwei LCDs, die sich ja im Enable-Parameter
unterscheiden, dann gleich zwei komplette Klassen, die nichts mehr
gemeinsam nutzen.
Die eierlegende Wollmilchsau gibt es halt nicht.
Klaus Wachtler schrieb:> Dafür hast bei z.B. bei zwei LCDs, die sich ja im Enable-Parameter> unterscheiden, dann gleich zwei komplette Klassen, die nichts mehr> gemeinsam nutzen.
Ja, man kann nicht alles haben.
In anderen Fällen ist dieser Aspekt verbreiteter, wie beispielsweise
Treibers für UARTs, denn davon gibts oft mehrere. Ich löse das dann über
eine normale Basisklasse, die den grössten Teil des Code enthält, und
einem davon abgeleiteten Template in dem beispielsweise die Puffer und
Interrupt-Wrapper liegen.
Klaus Wachtler schrieb:> Dafür hast bei z.B. bei zwei LCDs, die sich ja im Enable-Parameter> unterscheiden, dann gleich zwei komplette Klassen, die nichts mehr> gemeinsam nutzen.
Man kann sie aber auch als ein großes LCD betrachten.
Dann muß man nur die Funktionen lcd_nibble() und lcd_pos() etwas pimpen:
Wie läuft das dann mit dem compilieren ?
kann ich den avr gcc auch für c++ nutzen oder gibt es einen avr g++ ?
Ich hab mir das mit der oop einfach so gedacht, das ich für alles usart,
twi ,timer etc eine eigene basisklasse schreibe um mir alles
übersichtlicher und einfacher zu machen.
kann avr studio c++ code übersetzen ?
Ich habe beim googeln aufgeschnappt das man sich den new bzw. delete
operator selbst schreiben muss wenn man avr g++ nutzen möchte ?
Ist da was drann ? eigentlich müssten new und delete doch schon
implementiert sein, da es sich um g++ handelt ?
Dynamische Speicherverwaltung auf Systemen mit nur ein paar KB RAM ist
nicht sehr sinnvoll. Wenn man sie braucht, schreibt man sich eine auf
bestimmte Datentypen zugeschnittene Version selber.
In der AVR-Libc gibt es malloc und free, die du prinzipiell für die
new- und delete-Operatoren verwenden kannst. Aber du wirst glücklicher,
wenn du auf new und delete verzichtest.
Ich kann also auch über die c-funktion malloc ein Objekt (Instanz)
erzeugen ? Weshalb gibt es dann in c++ new bzw. delete ?
Was haltet ihr davon in z.b. einer usart klasse für den mega16/32 im
private bereich für jedes Register das den usart betrifft ein bitfeld
anzulegen, um dann über einsprechende methoden auf das entsprechende bit
im register zugreifen zu könnenn ?
Sven schrieb:> Weshalb gibt es dann in c++ new bzw. delete ?
Weil die gleich ctor bzw. dtor aufrufen, malloc und free nicht.
Außerdem gelingt new immer in dem Sinne, daß es entweder einen gültigen
Zeiger liefert, oder notfalls eine Ausnahme wirft.
Eben letzteres ist auch nicht unbedingt erstrebenswert auf einem AVR
(bzw. gar nicht möglich).
Markus B. schrieb:> Der Compiler kann noch so gut sein, aber den zusätzlichen Code kann er> auch nicht wegoptimieren.>> Nur mal ein Beispiel. Den Code macht nicht viel mehr als die Ausgabe von> zwei Texten und erzeugt folgendes:Size after:> AVR_LCD44780.elf :> section size addr> .text 12976 0> .data 20 8388704> .stab 10944 0> .stabstr 3405 0> Total 27345
Ich habe mal ein minimales Beispiel gemacht:
Sven schrieb:> Was haltet ihr davon in z.b. einer usart klasse für den mega16/32 im> private bereich für jedes Register das den usart betrifft ein bitfeld> anzulegen, um dann über einsprechende methoden auf das entsprechende bit> im register zugreifen zu könnenn ?
Das mache ich wesentlich lieber, als die übliche Bitschieberei, alleine
wegen der Lesbarkeit und m.E. geringeren Fehleranfälligkeit.
Scheint hier aber nicht sehr verbreitet zu sein...
Klaus Wachtler schrieb:> das darfst du mich nicht fragen :-)
Wieso, ich hab dein Makefile verwendet ;)
Ich hab nur das Archiv ausgepackt und make eingegeben.
In die ZIP-Datei von meinem Link habe ich nicht reingeschaut, sondern
eine Version bei mir auf der Platte genommen.
Da stand vom letzten Herumprobieren noch -O3 und die volle C-Lib mit
printf und float drin; das habe ich zu -Os und minimale C-Lib geändert.
Das kann gut der Unterschied sein.
Dann kommt die Größe aber ja nicht aus meiner LCD-Geschichte, sondern
unsinnigerweise aus der Lib.
Klaus Wachtler schrieb:> Ich habe mal ein minimales Beispiel gemacht:> ...> Das gibt bei mir mit -Os und kleinster C-Lib:> AVR_LCD44780.elf :> section size addr> .text 3034 0> .data 6 8388704> .stab 1716 0> .stabstr 84 0> Total 4840>> Also noch einige Luft auf einem atmega8...>> (übrigens mit dem viel gescholtenen gcc 4.3.2)
Gleiches Beispiel mit dem weniger gescholtenen (da noch nicht so weit
verbreiteten) GCC 4.6.1 (-Os, -mmcu=atmega8) und der Default-C-Lib
(*printf wird nicht gelinkt, da nicht benutzt):
1
AVR_LCD44780.elf :
2
section size addr
3
.text 992 0
4
.data 6 8388704
5
.stab 2100 0
6
.stabstr 156 0
7
Total 3254
Dass immer noch ein knappes KB übrig bleibt, ist darauf zurückzuführen,
dass die Anschlusspins des LCDs in der Klasse erst zur Laufzeit
festgelegt werden, was aufwendige Bitschiebereien (insbesondere in
writeByte) bedingt. Würde man die Pins als Konstanten definieren,
könnte man noch ein paar hundert Bytes einsparen, so dass die Codegröße
vermutlich in die Größenordnung des Codes von Peter rücken wird.
Das Beispiel (Sprung von 3034 auf 992 Bytes) zeigt übrigens, dass neuere
GCC-Versionen nicht immer schlechter optimieren als die alten :)
A. K. schrieb:> Stefan H. schrieb:>>> Wie Klaus schon gesagt hat: Man sollte beim Vererbungen und virtuellen>> Funktionen sehr vorsichtig sein, den diese erzeugen einen riesigen>> Overhead>> Riesig? Overhead ja, aber riesig ist der nicht. Braucht halt auf den> AVRs ein bisschen mehr RAM, weil die als Harvard-Kisten die VFTs nicht> ins ROM kriegen.
Der AVR hat damit kein Problem. Es ist der gcc, der das nicht
hinbekommt, weil er keine Unterstützung für mehere Speichertypen
besitzt.
Klaus Wachtler schrieb:> davon gehe ich aus; der avr-gcc unter Linux ist gleichzeitig C++> (avr-g++).
Nicht ganz. der avr-g++ ist der reine C++-Compiler. Der avr-gcc ist das
generische Front-End, das beim compilieren anhand des Dateinamens den
Typ erkennt. Aber eben nur beim compilieren, nicht beim linken. Da
braucht man avr-g++ (oder "avr-gcc -X c++").
sven schrieb:> Ich habe beim googeln aufgeschnappt das man sich den new bzw. delete> operator selbst schreiben muss wenn man avr g++ nutzen möchte ?> Ist da was drann ?
Ja.
> eigentlich müssten new und delete doch schon implementiert sein, da es> sich um g++ handelt ?
Die Implementation steht in der zum gcc gehörenden libsupc++, die aber
für den avr nicht existiert. Es hat einfach noch keiner die Anpassungen
gemacht, die für eine funktionierende Version nötig wären.
Sven schrieb:> Ich kann also auch über die c-funktion malloc ein Objekt (Instanz)> erzeugen ?
Nein. Du kannst Speicher dafür reservieren. Zum Erzeugen einer Instanz
gehören aber noch Konstruktoren und andere Dinge.
Yalu X. schrieb:> Dynamische Speicherverwaltung auf Systemen mit nur ein paar KB RAM ist> nicht sehr sinnvoll. Wenn man sie braucht, schreibt man sich eine auf> bestimmte Datentypen zugeschnittene Version selber.
Naja, wenn man Polymporphie einsetzen will, gehört dazu normalerweise
auch die dynamische Erzeugung mit new. Hier kann man auf dem AVR aber
z.B. mit placement new arbeiten. Dem übergibt man die Adresse von schon
reserviertem Speicher. So kann man das Objekt dynamisch erzeugen, aber
in statischem Speicher halten. Man muß nur dafür sorgen, daß der
Speicher groß genug ist für das Objekt.
Rolf Magnus schrieb:> Klaus Wachtler schrieb:>> davon gehe ich aus; der avr-gcc unter Linux ist gleichzeitig C++>> (avr-g++).>> Nicht ganz. der avr-g++ ist der reine C++-Compiler. Der avr-gcc ist das> generische Front-End, das beim compilieren anhand des Dateinamens den> Typ erkennt. Aber eben nur beim compilieren, nicht beim linken. Da> braucht man avr-g++ (oder "avr-gcc -X c++").
Ich habe mein Beispiel mit avr-gcc erzeugt.
Du hast natürlich recht: das ist nur das Frontend, es erkennt an der
Dateiendung, ob es sich um C oder C++ handelt und kompiliert
entsprechend.
Was das Kompilieren angeht, ist es also erstmal egal, ob ich avr-gcc
oder avr-g++ aufrufe.
Weiterhin hast du auch recht, was das Linken angeht:
Bei avr-gcc werden standardmäßig nur die C-Libs dazugepackt, bei avr-g++
eben auch die für C++.
Das ist aber beim AVR nicht relevant, weil eh nur gegen die avrlibc
gelinkt wird.
Auf einem PC bspw. kann man ebenfalls eine C++-Datei mit gcc (statt g++)
kompilieren, muß dann aber beim Linken mit -l... die C++-Lib dazu
bestellen.
Das ist hier aber irrelevant; deshalb sind in meinem Beispiel avr-gcc
und avr-g++ letztlich gleichwertig.
Klaus Wachtler schrieb:> Das war mit meiner C++-Version der LCD-Lib?> Dann muß ich auch den gcc updaten!
Yep. Damit du siehst, welche Compiler- und Linker-Optionen ich geändert
habe, habe ich das geänderte Makefile angehängt.
GCC 4.3.6 mit dem gleichen Makefile liefert bei mir folgendes Ergebnis:
1
AVR_LCD44780.elf :
2
section size addr
3
.text 2718 0
4
.data 6 8388704
5
.stab 1740 0
6
.stabstr 84 0
7
Total 4548
Die AVR-Libc ist jeweils Version 1.7.1 und mit GCC 4.2.4 gebaut.
Der Unterschied zwischen GCC 4.3.6 und 4.6.1 liegt vor allem darin
begründet, dass 4.3.6 den Konstruktor der LCD-Klasse nicht inlinet,
weswegen das rieseige Case-Konstrukt in voller Länge stehen bleibt.
Stattdessen inlinet 4.3.6 ungünstigerweise die Methode enable, die
sehr oft aufgerufen wird.
Wenn man LCD44780 und enable mit den Attributen always_inline bzw.
noinline versieht, sieht auch beim 4.3.6 das Ergebnis deutlich besser
aus:
Rolf Magnus schrieb:> A. K. schrieb:>> Riesig? Overhead ja, aber riesig ist der nicht. Braucht halt auf den>> AVRs ein bisschen mehr RAM, weil die als Harvard-Kisten die VFTs nicht>> ins ROM kriegen.>> Der AVR hat damit kein Problem. Es ist der gcc, der das nicht> hinbekommt, weil er keine Unterstützung für mehere Speichertypen> besitzt.
GCC könnte es schon, nur ist das im avr-Teil (noch) nicht eingebaut.
Allerdings wäre das erst die halbe Miete für "vtables im Flash",
aber immerhin würde es das ganze pgm_read-Geraffel überflüssig machen.
> Da braucht man avr-g++ (oder "avr-gcc -X c++").
avr-gcc -x c++
Johann L. schrieb:>> Der AVR hat damit kein Problem. Es ist der gcc, der das nicht>> hinbekommt, weil er keine Unterstützung für mehere Speichertypen>> besitzt.>> GCC könnte es schon, nur ist das im avr-Teil (noch) nicht eingebaut.
Ist das neu? Als ich das letzte Mal (genau wegen dieser Thematik) in den
gcc-Eingeweiden rumgewühlt hab, hab ich da nichts gefunden.
>> Da braucht man avr-g++ (oder "avr-gcc -X c++").>> avr-gcc -x c++
Richtig. War ein Tippfehler.
Peter Dannegger schrieb:> Etwas lästig ist, daß die Pindefinitionen immer zweimal gemacht werden> müssen, je einmal für Ausgangs- und Richtungsbit.
Und ggf. ein drittes Mal für die Eingangsbits.
Das hat mich auch schon lange geärgert; man kann es vermeiden, indem man
das Register (PORT/DDR/PIN) als Parameter ins Macro nimmt:
1
#define _AP_ __attribute__((__packed__))
2
// JD 20110709 erweitert// Access bits like variables: aus Dannegger
3
typedefunionuBits_ut{
4
// .bX (b0, b1, ..): einzelbits, X:Bit-Nummer
5
// .bXY (b21, ..): Gruppen von bits; X:Anzahl der Bits in der Gruppe, Y: niedrigstes Bit der Gruppe
Rolf Magnus schrieb:> Nein. Du kannst Speicher dafür reservieren. Zum Erzeugen einer Instanz> gehören aber noch Konstruktoren und andere Dinge.
Das heißt ich implementiere in meiner Klasse einen Konstruktor bzw.
Destruktor wie sont auch bei c++.
Rufe den Konstruktor aber über maloc auf ?
Kann jemand ein kurzes Beispiel geben, wie man eine Instanz über malloc
erzeugt ?
sven_w schrieb:> Rufe den Konstruktor aber über maloc auf ?
Nein, du implementierst die Operatoren new und delete mit Hilfe von
malloc und free, und benutzt dann ganz normal new/delete.
Stefan Ernst schrieb:> Nein, du implementierst die Operatoren new und delete mit Hilfe von> malloc und free, und benutzt dann ganz normal new/delete.
Wobei jetzt noch der für 'new' und 'delete' typische Aufruf des
Konstruktors/ Destruktors fehlt. (Ich hoffe, ich irre mich nicht.)
Stefan Ernst schrieb:> sven_w schrieb:>> Rufe den Konstruktor aber über maloc auf ?>> Nein, du implementierst die Operatoren new und delete mit Hilfe von> malloc und free, und benutzt dann ganz normal new/delete.> void * operator new (size_t size) {>> return malloc(size);> }>> void operator delete (void * ptr) {>> free(ptr);> }
Aua, bloß nicht!
Wenn ein Ding "new" heißt, sollte es IMMER new-Verhalten haben, d.h.
allozieren UND konstruieren.
Konstruieren heißt unter anderem, dass die VTable gesetzt wird, wenn das
Objekt virtuelle Funktionen hat. Daran führt KEIN Weg vorbei, das ist
der Overhead, den man bezahlen MUSS, wenn man C++ verwenden will. Eine
nicht gesetzte VTable lässt das Programm todsicher krachen.
Ralf G. schrieb:> Wobei jetzt noch der für 'new' und 'delete' typische Aufruf des> Konstruktors/ Destruktors fehlt. (Ich hoffe, ich irre mich nicht.)willibald schrieb:> Aua, bloß nicht!>> Wenn ein Ding "new" heißt, sollte es IMMER new-Verhalten haben, d.h.> allozieren UND konstruieren.
Ich benutze C++ zwar nicht wirklich, aber ich bin mir doch ziemlich
sicher, dass der ganze Rest vom Compiler bei der Benutzung des new dann
selber gemacht wird. Für die Operator-Implementierung reicht das
Allozieren. Wie sollte man auch mehr machen, wo man außer der Größe ja
auch null Informationen über das anzulegende Objekt hat.
Stefan Ernst schrieb:> Ich benutze C++ zwar nicht wirklich, aber ich bin mir doch ziemlich> sicher, dass der ganze Rest vom Compiler bei der Benutzung des new dann> selber gemacht wird.
Korrekt.
willibald schrieb:> Stefan Ernst schrieb:>> sven_w schrieb:>>> Rufe den Konstruktor aber über maloc auf ?>>>> Nein, du implementierst die Operatoren new und delete mit Hilfe von>> malloc und free, und benutzt dann ganz normal new/delete.>> void * operator new (size_t size) {>>>> return malloc(size);>> }>>>> void operator delete (void * ptr) {>>>> free(ptr);>> }>> Aua, bloß nicht!
Doch
> Wenn ein Ding "new" heißt, sollte es IMMER new-Verhalten haben, d.h.> allozieren UND konstruieren.
Das vom C++ geforderte Verhalten des globalen operator new besteht
darin, Speicher zu besorgen. Er muss keinen Konstruktor bedienen, das
ist nicht seine Aufgabe. Abgesehen davon könnte er das auch gar nicht,
weil er nicht weiß, für welches Objekt er aufgerufen wurde.
Nochmal: Die Operatoren new bzw delete haben die Aufgabe Speicher zu
allokieren. Damit ist ihre Schuldigkeit getan. Um den Rest muss sich der
Compiler kümmern.
Edit: zu langsam. Hab zu lange zum Lesen des kompletten Thread gebraucht
:-)
Markus B. schrieb:> Um mal wieder zum Topic zu kommen>> http://xpcc.sourceforge.net/
Als einer der Autoren dieses Projektes und mittlerweile großer Fan von
C++ auf Mikrocontrollern muss ich auch mal was dazu sagen ;-)
Unter
http://sourceforge.net/apps/trac/xpcc/browser/trunk/examples/lcd/hd44780/main.cpp
gibt es das Beispiel mit dem LCD mal mit einem etwas anderen Ansatz.
Wir verwenden dort Macros um für jeden IO-Pin Structs mit statischen
inline Funktionen zu erzeugen. Diese Structs kann man dann an die
Hardware-Treiber als Template Argument übergeben.
Das Beispiel oben verlinkte Beispiel erzeugt mit einem avr-gcc 4.3.4 und
aktivierten Optimierungen insgesamt 1610 Byte. Es ist allerdings schon
die Luxus Variante mit Stream Operatoren zur Ausgabe von belieben
Zahlen, virtuellen Funktionen usw.
Um den Ansatz besser vergleichen zu können habe ich mal eine minimale
Variante zusammengestrickt, siehe Anhang (wer das selbst compilieren
will braucht Python und SCons und muss den Wert von rootpath in der
SConstruct Datei so anpassen, dass er auf das xpcc-Verzeichnis
verweist). Diese Variante kann jetzt nicht mehr als einzelnen Zeichen
und Strings auf ein Display auszugeben. Damit bleiben von den 1610 nur
noch 584 Byte übrig.
// The LCD needs at least 50ms after power-up until it can be
20
// initialized.
21
xpcc::delay_ms(50);
22
23
display.initialize();
24
display.setCursor(0,0);
25
26
// write the standard welcome message ;-)
27
display.write("Hello World!\n");
28
29
while(1)
30
{
31
}
32
}
Damit spart man sich ganz herum gewurschtele mit dem Preprozessor und
kann einfach und übersichtlich die Pins einstellen.
Ganz fair ist der Vergleich aber nicht, da dort schon eine optimierte
Variante zur Ansprechen des Port-Nibble verwendet wird. Es ist aber
nicht schwierig beliebige Pins auch für die Daten zu verwenden. Dazu
muss man das Programm nur folgendermaßen abändern (Pins zufällig
gewählt):
willibald schrieb:> Konstruieren heißt unter anderem, dass die VTable gesetzt wird, wenn das>> Objekt virtuelle Funktionen hat. Daran führt KEIN Weg vorbei, das ist>> der Overhead, den man bezahlen MUSS, wenn man C++ verwenden will. Eine>> nicht gesetzte VTable lässt das Programm todsicher krachen.Beitrag melden |
Bearbeiten | Löschen |
Was ist eine VTable ?
Virtuelle Funktionen werden meistens indirekt aufgerufen. Teil eines
Objekts einer Klasse mit virtuellen Funktionen ist ein Pointer auf eine
Tabelle mit Pointern auf Funktionen. Die VFT (virtual function table)
oder vtable.
Der Aufruf pA->foo() landet bei B::foo
Wie wird das unter der Decke implementiert
Jede Klasse hat eine VTable, in der Pointer zu den virtuellen Funktionen
sind. Die VTable für A hat also einen Pointer zu A::foo. Die VTable für
B hat einen Funktionspointer zu B::foo
Jedes Objekt wiederrum besitzt einen Pointer zu der für es gültigen
VTable. myB hat also einen Pointer auf die B-VTable, weil myB ja ein B
Objekt ist.
Wenn der Compiler den Funktionsaufruf pA->foo() übersetzt, dann macht er
das wegen der Virtualität der Funktion foo so:
Mittels des POinters im Objekt den Pointer zur VTable holen (der Pointer
steht in allen Objekten immer an gleicher Stelle, normalerweise am
Objektanfang im Speicher). Mit dem jetzt bekannten VTable Pointer holt
er den zum Objekt gehörenden Funktionspointer und mit diesem wiederrum
wird dann die Funktion aufgerufen.
So wird erreicht, dass je nachdem auf welches Objekt (welcher
Klassentyp) pA tatsächlich zeigt, das Objekt selbst steuert, dass seine
zugehörige Funktion aufgerufen wird.
In C würde man das so machen:
1
typedefvoid(*voidPtr)(void);
2
3
// die generische VTable, die für Klasse A bz. B zuständig ist
4
structVTable
5
{
6
voidPtrfooFnct;
7
};
8
9
// die beiden virtuellen Funktionen
10
voidfoo_A()
11
{
12
printf("Dies ist A\n");
13
}
14
15
voidfoo_B()
16
{
17
printf("Dies ist B\n");
18
}
19
20
// für 'class' A bzw B jeweils eine VTable anlegen und mit den korrekten
21
// Funktionspointern befüllen
22
structVTableVTable_A{foo_A};
23
structVTableVTable_B{foo_B};
24
25
// dann brauchen wir noch die Klassen selber
26
structclass_A
27
{
28
structVTable*pVTable;
29
}
30
31
structclass_B
32
{
33
structVTable*pVTable;
34
}
35
36
// alle Zutaten beieinander. Jetzt kanns losgehen
37
intmain()
38
{
39
structclass_BmyB={&VTable_B};
40
structclass_A*pA=(structclass_A*)&myB;
41
42
//pA->foo()
43
structVTable*pVTab=pA->pVTable;// geht deshalb weil pVTable sowohl
44
// in struct class_A als auch in
45
// struct class_B denselben Offset vom
46
// Objektbeginn weg hat.
47
voidPtrfoo=pVTab->fooFnct;
48
(*foo)();
49
}
(DIe Behandlung des this Pointers hab ich mir in der C Variante
geschenkt)
Karl Heinz Buchegger schrieb:> Nochmal: Die Operatoren new bzw delete haben die Aufgabe Speicher zu> allokieren. Damit ist ihre Schuldigkeit getan. Um den Rest muss sich der> Compiler kümmern.
Und bei so einem selbstgebastelten 'new' und 'delete' weiß der Compiler
plötzlich, daß er jetzt fix noch Konstruktor und Destruktor aufrufen
muss?
Ralf G. (ralg) schrieb:> Karl Heinz Buchegger schrieb:>> Nochmal: Die Operatoren new bzw delete haben die Aufgabe Speicher zu>> allokieren. Damit ist ihre Schuldigkeit getan. Um den Rest muss sich der>> Compiler kümmern.> Und bei so einem selbstgebastelten 'new' und 'delete' weiß der Compiler> plötzlich, daß er jetzt fix noch Konstruktor und Destruktor aufrufen> muss?
Der Compiler weiss, dass er
A = new Irgendwas;
so übersetzen muss:
globalen operator new aufrufen, um den Speicher zu besorgen
auf diesem Speicher, dann den Konstruktor loslassen.
In den ersten Teil kannst du eingreifen, in dem du selbst einen globalen
operator new schreibst. Der zweite Teil ist obligatorisch und wird vom
Compiler immer gemacht; da kannst du nicht eingreifen. Schliesslich gibt
es da ja auch viele Möglichkeiten, je nachdem welche Konstruktoren
überhaupt verfügbar sind.
'Verkompliziert' wird das ganze dann noch dadurch, dass jede Klasse für
sich wieder die Möglichkeit hat, einen klassenspezifischen operator new
zu implementieren.
D.h. der Compiler sieht zuerst in der Klasse selber nach, ob es dort
einen operator new gibt, der den Speicher besorgt. Findet er dort
nichts, dann kommt der globale operator new zum Zug.
Und wieso 'plötzlich'? Klar weiß das der Compiler. Warum auch nicht? Er
weiß ja auch, dass bei delete ein Destruktor aufgerufen werden muss. Da
ist dann dasselbe Spielchen umgekehrt. operator delete kümmert sich um
den Speicher und der Compiler darum, dass Destruktor und operator delete
in dieser Reihenfolge aufgerufen wird.
Construktor bzw. Destruktor haben doch nichts damit zu tun, wo der
Speicher herkommt, in dem das Objekt erzeugt wird.
Ralf G. (ralg) schrieb:> Und bei so einem selbstgebastelten 'new' und 'delete' weiß der Compiler> plötzlich, daß er jetzt fix noch Konstruktor und Destruktor aufrufen> muss?
Korrekt. Und warum plötzlich? Das ist eben so definiert.
willibald schrieb:> Wenn ein Ding "new" heißt, sollte es IMMER new-Verhalten haben, d.h.> allozieren UND konstruieren.
Wenn du nicht weißt, wie man einen eigenen Operator new definiert, dann
solltest du nicht versuchen, das anderen zu erklären.
> Konstruieren heißt unter anderem, dass die VTable gesetzt wird, wenn das> Objekt virtuelle Funktionen hat. Daran führt KEIN Weg vorbei, das ist> der Overhead, den man bezahlen MUSS, wenn man C++ verwenden will.
Es ist der Overhead, den man bezahlen muß, wenn man Polymorphie in C++
nutzen will. Eine Klasse ohne virtuelle Memberfunktionen hat auch keine
vtable. Und wenn man in C Polymporphie "zu Fuß" implementiert, wird man
dadurch den gleichen Overhead bekommen.
Karl Heinz Buchegger schrieb:> Der Compiler weiss, dass er>> A = new Irgendwas;>> so übersetzen muss:>> globalen operator new aufrufen, um den Speicher zu besorgen> auf diesem Speicher, dann den Konstruktor loslassen.
Aha, also das klingt so als ob 'new' und 'delete' dem AVR_C++Compiler
zwar bekannt sind (so als Wörter), er aber den entsprechenden Befehl
dazu nicht kennt? Und wenn man Speicher reserviert mit einem
'new'-Operator dann erinnert der sich auch den Konstruktor aufzurufen?
Hmm, Zauberei.
Rolf Magnus schrieb:> vtable. Und wenn man in C Polymporphie "zu Fuß" implementiert, wird man> dadurch den gleichen Overhead bekommen.
Das ist nämlich der springende Punkt, der gerne übersehen wird.
Natürlich kostet polymorphes Verhalten etwas. Aber die Alternativen
kosten auch etwas (meistens mehr) und im Zweifelsfall überlass ich das
alles lieber dem Compiler, als dass ich selbst an diese fehlerträchtige
Arbeit gehe. Und Polymorphismus Marke Eigenbau IST fehlerträchtig, egal
ob mit VTable oder switch/case Typ-Verteilern (die sind noch
unflexibler).
Ralf G. (ralg) schrieb:> Karl Heinz Buchegger schrieb:>> Der Compiler weiss, dass er>>>> A = new Irgendwas;>>>> so übersetzen muss:>>>> globalen operator new aufrufen, um den Speicher zu besorgen>> auf diesem Speicher, dann den Konstruktor loslassen.> Aha, also das klingt so als ob 'new' und 'delete' dem AVR_C++Compiler> zwar bekannt sind (so als Wörter),
natürlich. Das sind Schlüsselwörter der Sprachdefinition
> er aber den entsprechenden Befehl> dazu nicht kennt?
Auch das weiß er. Er weiß, dass er im Zuge der Behandlung des
Schlüsselwortes 'new', den operator new aufrufen muss, der eine ganz
normale Funktion ist.
Nur muss eben irgendwer irgendwann einmal diese Funktion geschrieben
haben.
> Und wenn man Speicher reserviert mit einem> 'new'-Operator dann erinnert der sich auch den Konstruktor aufzurufen?> Hmm, Zauberei.
Dein Denkfehler:
du verwechselst die Schlüsselwörter 'new' und 'delete' mit den
Operatoren new und delete.
Das Schlüsselwort new ist in
a = new irgendwas;
nicht synonym damit, dass da nur der Operator new aufgerufen wird. Da
passiert schon noch mehr. Drum sieht ja auch die Syntax nicht wie ein
Funktionsaufruf aus :-)
Was stört dich an der Sache?
Bei
for( i = 0; i < 8; ++i )
weiß der Compiler ja auch, was er mit den einzelnen Teilen zu machen
hat.
Karl Heinz Buchegger schrieb:> du verwechselst die Schlüsselwörter 'new' und 'delete' mit den> Operatoren new und delete.
Also Verwechslung. Ist das nachzuvollziehen, dass ich das verwechselt
habe oder fehlt mir da einfach der Durchblick?
Ich versuche zu verstehen, wozu man sich einen Operator 'new' bastelt,
von dem man nur ein bisschen Speicher zurückbekommt. Ich würde da mehr
erwarten. Oder ist in AVR-C++ 'new' zwar implementiert aber das
Speicherallozieren wurde 'vergessen'? ...???
Ralf G. (ralg) schrieb:> Also Verwechslung. Ist das nachzuvollziehen, dass ich das verwechselt> habe oder fehlt mir da einfach der Durchblick?
Solange man da selbst niemals ran muss (um zb Chunk Allokierung aus
Performance Gründen zu machen), ist der operator new ein Abschnitt im
Lehrbuch das man liest und gleich wieder vergisst.
> Ich versuche zu verstehen, wozu man sich einen Operator 'new' bastelt,> von dem man nur ein bisschen Speicher zurückbekommt.
Das ist seine Aufgabe. Mehr muss er nicht tun.
Und 'nur ein bischen Speicher zurükbekommt' kann je nach System schon
eine anspruchsvolle Aufgabe für sich selbst sein.
Du kannst zb in operator new deine eigene Speicherverwaltung
implementieren, die völlig losgelöst von dem agiert, was dir das
Betriebssystem aufs Auge drückt.
Oder du kannst natürlich auch einfach malloc bemühen. Soll sich doch
malloc mit dem BS rumstreiten :-)
Auf einem AVR könnte man zb ein 100 Byte großes Array als dynamischen
Speicher abstellen, wenn man weiß, dass man in Summe niemals mehr als
100 Bytes dynamisch allokiert. Dann hat man dort eine Möglichkeit, wie
man den kompletten Speicherverbrauch des Programms trotz dynamischer
Allokierung schon während des Compilierens feststellen kann.
Oder ....
Möglichkeiten gibt es viele.
> Ich würde da mehr> erwarten. Oder ist in AVR-C++ 'new' zwar implementiert aber das> Speicherallozieren wurde 'vergessen'? ...???
Ob Absicht oder nicht kann ich nicht sagen. Tatsache ist, dass es in der
Auslieferversion keine Implementierung dafür gibt.
Karl Heinz Buchegger schrieb:> Ob Absicht oder nicht kann ich nicht sagen. Tatsache ist, dass es keine> Implementierung dafür gibt.
Ich bastle mir zu einem kompletten 'new' noch den Operator 'new' dazu
und dann funktioniert alles so, wie man es gewohnt ist? Ich kann's gar
nicht glauben!
Ralf G. (ralg) schrieb:> Aha, also das klingt so als ob 'new' und 'delete' dem AVR_C++Compiler> zwar bekannt sind (so als Wörter), er aber den entsprechenden Befehl> dazu nicht kennt?
new und delete sind Schlüsselwörter. Somit kennt g++ die immer. Beim
avr-g++ fehlt lediglich die dahinterstehende Implementation. Wenn du
also new einfach so benutzt, bekommst du eine Fehlermeldung vom Linker.
Ralf G. (ralg) schrieb:> Ich versuche zu verstehen, wozu man sich einen Operator 'new' bastelt,> von dem man nur ein bisschen Speicher zurückbekommt. Ich würde da mehr> erwarten.
Aber genau das ist seine Aufgabe. Was sollte er mehr machen?
> Oder ist in AVR-C++ 'new' zwar implementiert aber das Speicherallozieren> wurde 'vergessen'? ...???
Es ist Teil einer Bibliothek, die sich für AVR nicht bauen läßt. Darum
fehlt unter anderem eben diese Funktion (was z.B. auch fehlt, sind
Exceptions, welche der standardmäßige Operator new übrigens auch
benötigt). Es hat sich eben noch keiner hingesetzt und darum gekümmert,
daß es das für avr gibt.
Karl Heinz Buchegger schrieb:> du verwechselst die Schlüsselwörter 'new' und 'delete' mit den> Operatoren new und delete.
Genauer gesagt den new-Ausdruck mit Operator new. In der ISO-Norm werden
die so bezeichnest ("new-expression" und "operator new"). Hier ein
Auszug:
"Objects can be created dynamically during program execution (1.9),
using new-expressions (5.3.4), and destroyed using delete-expressions
(5.3.5). A C++ implementation provides access to, and management of,
dynamic storage via the global allocation functions operator new and
operator new[] and the
global deallocation functions operator delete and operator delete[]."
Ralf G. (ralg) schrieb:> Karl Heinz Buchegger schrieb:>> Ob Absicht oder nicht kann ich nicht sagen. Tatsache ist, dass es keine>> Implementierung dafür gibt.> Ich bastle mir zu einem kompletten 'new' noch den Operator 'new' dazu> und dann funktioniert alles so, wie man es gewohnt ist? Ich kann's gar> nicht glauben!
Du brauchst auch noch operator delete :-)
Ja. Was verblüfft dich daran jetzt?
Sieh den op new einfach als Phase an, die bei einer Objektkonstruktion
durchlaufen wird und bei der dir C++ die Möglichkeit gibt, dort
einzugreifen.
Mehr steckt da nicht dahinter. Das ist die Absicht hinter dem Ganzen.
Rolf Magnus schrieb:> Genauer gesagt den new-Ausdruck mit Operator new. In der ISO-Norm werden> die so bezeichnest ("new-expression" und "operator new"). Hier ein> Auszug:
Richtig.
Ist schon lange her, dass ich das letzt mal in der Ecke im Standard war.
Karl Heinz Buchegger schrieb:> Ja. Was verblüfft dich daran jetzt?
Wenn ich schreibe:
a=new b;
dann wird Speicher reseviert und der Konstruktor für den Typ b
aufgerufen.
Das 'Bastel'-new holt sich doch nur den Speicher. Das ist doch
'Vortäuschung falscher Tatsachen'?
Ralf G. (ralg) schrieb:> Karl Heinz Buchegger schrieb:>> Ja. Was verblüfft dich daran jetzt?> Wenn ich schreibe:> a=new b;> dann wird Speicher reseviert und der Konstruktor für den Typ b> aufgerufen.> Das 'Bastel'-new holt sich doch nur den Speicher. Das ist doch> 'Vortäuschung falscher Tatsachen'?
Wie oft denn noch?
Objektkonstruktion ist in 2 Phasen gegliedert.
Speicher besorgen
Konstruktor aufrufen.
Bei Speicher besorgen gibt es meherer Möglichkeiten
das kann Speicher vom Stack sein
das kann Speicher sein, der mittels Placement new vom Aufrufer
gestellt wird, wo sich der Compiler also überhaupt nicht
darum kümmern muss.
das kann Speicher sein, den der operator new besorgen muss
(nämlich dann, wenn die Objektkonstruktion über eine
normale new Expression erzwungen wird)
Konstruktor aufrufen kann ebenfalls vieles sein
das kann der Default konstruktor sein
das kann ein Copy Konstruktor sein
das kann ein benutzerdefinierter Konstruktor mit einer
nicht näher (im Sprachstandard) festgelegten Anzahl von
Paramatern und Datentypen sein.
(Und das ganze gibt es dann auch noch in Array-Versionen)
Willst du wirklich für all die unterschiedlichen Konstruktoraufrufe
jeweils einen eigenen op new schreiben?
Das 'Bastel-New' ist der Teil, der im Fall "Speicher Besorgen und zwar
aufgrund einer Objektkonstruktion mittels einer new-Expression" ganz
offiziell dafür zuständig ist, den Speicher zu besorgen. Das ist NICHT
gebastelt! Das ist ganz offiziell so vorgesehen und im Sprachstandard
dokumentiert. Was ist denn schon dabei, wenn der Sprachstandard da einen
Sytemimplementieren dazu zwingt diesen Operator zu schreiben? Wenn du
qsort verwenden willst, musst du doch auch eine Vergleichsfunktion
bereitstellen, die für qsort eine bestimmte wohldefinierte Aufgabe
erledigt. Und hier ist das nichts anderes. In bestimmten Fällen benutzt
der Compiler einen Aufruf einer von irgendjemand bereitsgestellten
'Funktion' namens "operator new" um den Teil 'Speicher organsieren
während einer Objektkonstruktion' in bestimmten Fällen erledigen zu
lassen. Hat der Compiler den Pointer zum Speicher erst mal in Händen,
dann ruft er auf diesem Speicher den anhand gewisser Kriterien
ausgewählten Konstruktor auf. Das hat aber nichts mehr mit dem op new zu
tun. Der hat seinen Job dann schon längst erledigt.
Karl Heinz Buchegger schrieb:> Willst du wirklich für all die unterschiedlichen Konstruktoraufrufe> jeweils einen eigenen op new schreiben?
Da schreiben wir aneinander vorbei. Ich weiß ja jetzt, dass ich das
Schlüsselwort mit dem Operator gleichgesetzt habe. Also, ein
'new'-Aufruf auf dem AVR führt zur Fehlermeldung, weil es dazu keinen
Code gibt. Damit man 'new' trotzdem verwenden kann, macht man diese
'new-malloc-Speicheranmeldung' draus. Aber eben nicht mehr, weil das
nicht ins Programm gehöhrt. Da muss sich der Compiler drum kümmern. Nur
kann das der AVR-Compiler nicht, weil man es ihm nicht beigebracht hat.
Ist das bis hierher richtig?
Falls ja: Wozu dann dieses Hilfs-new, wenn man (wie auf anderen
Systemen), mehr erwarten müsste?
Falls nein: Ich brauch 'ne Pause!
Rolf Magnus schrieb:> Ich glaube, er hätte weniger Probleme damit, wenn die Funtionen karl()> und franz() statt operator new() und operator delete() hießen.
Genau, das ist die Antwort. [ karl() und rolf() natürlich :-) ]
Deswegen:
Ralf G. (ralg) schrieb:> Das 'Bastel'-new holt sich doch nur den Speicher. Das ist doch> 'Vortäuschung falscher Tatsachen'?
Und meine Bedenken:
Da tauchen in einem Quelltext 'new' und 'delete' auf, und jeder wundert
sich, wieso das überhaupt sein kann! Naja, wahrscheinlich nur ich...
Ralf G. (ralg) schrieb:> Da schreiben wir aneinander vorbei. Ich weiß ja jetzt, dass ich das> Schlüsselwort mit dem Operator gleichgesetzt habe. Also, ein> 'new'-Aufruf auf dem AVR führt zur Fehlermeldung, weil es dazu keinen> Code gibt. Damit man 'new' trotzdem verwenden kann, macht man diese> 'new-malloc-Speicheranmeldung' draus. Aber eben nicht mehr, weil das> nicht ins Programm gehöhrt. Da muss sich der Compiler drum kümmern. Nur> kann das der AVR-Compiler nicht, weil man es ihm nicht beigebracht hat.> Ist das bis hierher richtig?
Der Compiler kann das alles.
Aber in den Systemlibraries findet der Linker keine Implementierung für
die Funktion 'operator new' (Ein Operator ist auch nur eine Funktion),
die vom Compiler zu Hilfszwecken (eben dem Besorgen von Speicher)
benutzt wird, wenn du das new Schlüsselwort benutzt.
> Falls ja: Wozu dann dieses Hilfs-new, wenn man (wie auf anderen> Systemen), mehr erwarten müsste?
Man erwartet eben nicht mehr!
Man muss nicht mehr erwarten!
diese implementierung von new macht genau das, was sie tun soll. Nicht
mehr und nicht weniger.
Ralf G. (ralg) schrieb:> Da muss sich der Compiler drum kümmern. Nur> kann das der AVR-Compiler nicht, weil man es ihm nicht beigebracht hat.
Wo hast du das denn jetzt raus gelesen? Klar kann er das.
OK. Gedankenswitch
Stell dir einfach vor die Funktion hiesse nicht "operator new" sondern
"operator allocateMemory".
That's it. Das ist ihre Aufgabe. Es ist eine Hilfsfunktion für den
Compiler, die er aufrufen kann, wenn er an irgendeiner Stelle im Code
eine Anforderung zur dynamischen Objekterzeugung vorfindet. Er benutzt
diese Hilfsfunktion um zunächst den Speicher zu reservieren in dem dann
in weiterer Folge das Objekt erzeugt wird.
Ralf G. (ralg) schrieb:> Falls ja: Wozu dann dieses Hilfs-new, wenn man (wie auf anderen> Systemen), mehr erwarten müsste?
Man erwartet aber nicht mehr, weder auf dem AVR, noch auf irgendeinem
anderen System. Diese Möglichkeit, einen eigenen operator new zu
defnieren, ist keine Ausgeburt von avr-g++, sondern ist in der ISO-Norm
zu C++ genau so festgelegt und wird von jedem konformen Compiler auch
genau so unterstützt. Der einzige Unterschied ist, daß es nach
ISO-Definition eigentlich schon einen vordefinierten operator new geben
müßte, der automatisch einspringt, wenn man nicht selbst einen
definiert. Dieser vordefinierte operator new fehlt beim avr-g++, also
muß man sich immer selbst einen definieren.
Ralf G. (ralg) schrieb:> Rolf Magnus schrieb:>> Ich glaube, er hätte weniger Probleme damit, wenn die Funtionen karl()>> und franz() statt operator new() und operator delete() hießen.> Genau, das ist die Antwort. [ karl() und rolf() natürlich :-) ]
Ja, natürlich ;-)
Vielleicht zur Klarstellung:
C++ will nicht ermöglichen, das Verhalten der Sprache komplett
umkrempeln zu lassen. Beispielsweise steht nicht zur Wahl, ob man den
ctor/dtor-Mechanismus aushebeln will oder ob new vielleicht gar nicht
Objekte erzeugt, sondern stattdessen Bier bestellt.
Es gibt aber in dem vorgegebenen Mechanismus einige Stellen, an denen
man in Grenzen eingreifen kann.
Der eine Punkt ist: wie soll ein Objekt initialisiert werden oder
aufgelöst?
Dazu überschreibt man einen Konstruktor bzw. Destruktor.
Der andere Punkt ist eben: woher nimmt man den Speicher?
Je nach System ist vielleicht nicht der ganze Speicher gleichwertig. Bei
manchen AVR kann man z.B. zwischen schnellerem internen und ggf.
externem Speicher wählen, oder auf anderen Systemen mit shared memory
oder memory mapped files spielen. Das wird ermöglicht, indem man
operator new umschreibt.
Das alles ändert aber nichts am vorgegeben Rahmen.
Bei anderen Operatoren ist es ähnlich (soweit man das überhaupt
vergleichen kann): Man kann neu definieren, was sie im Detail machen,
aber weder kann man eine Addition mit 5 Operanden bauen, noch die
Rangfolge der Operatoren ändern oder neue schaffen.
(1) willibald schrieb:> Aua, bloß nicht!>> Wenn ein Ding "new" heißt, sollte es IMMER new-Verhalten haben, d.h.> allozieren UND konstruieren.(2) Karl Heinz Buchegger schrieb:> Der Compiler weiss, dass er>> A = new Irgendwas;>> so übersetzen muss:>> globalen operator new aufrufen, um den Speicher zu besorgen> auf diesem Speicher, dann den Konstruktor loslassen.(3) Rolf Magnus schrieb:> Ich glaube, er hätte weniger Probleme damit, wenn die Funtionen karl()> und franz() statt operator new() und operator delete() hießen.
Mit neuem Erkenntnisstand, habe ich mir noch mal alles durchgelesen :-)
Das waren, nur z.B., so die Dinge, die mich verwirrten:
zu (1): so hatte ich mir das auch vorgestellt
zu (2): das meinte ich, was 'new' können soll [durch
'Schlüsselwort/Operator-Gleichsetzung' (sehr) unklar ausgedrückt]
zu (3): klingt ja wieder wie (1) nur mit anderen Namen, fehlt dann nicht
der Konstruktor/Destruktor-Aufruf?
Ich entscheide mich jetzt für (2) :)
Hallo Ralf,
Ralf G. schrieb:> Ich entscheide mich jetzt für (2) :)
hier etwas Pseudocode, wie der Compiler aufgebaut sein könnte.
1
TYPE *create_new_object() {
2
TYPE *ptr;
3
ptr = operator new(sizeof(TYPE));
4
initialize(ptr); // Membervariablen einrichten
5
ptr->TYPE(); // Constructor
6
7
return ptr;
8
}
Diese Routine ist im Prinzip fest im Compiler verdrahtet und das
Einzige, was du daran ändern kannst, ist der operator new, der allein
die Aufgabe hat, ein n Byte großes Speicherstück zu besorgen.
Genau diese Speicherholfunktion fehlt im avr-g++; den Ablauf, ein Objekt
in C++ zu erzeugen, kennt der Compiler jedoch.
Vielleicht packen wir's mal ganz anders an:
Wenn ich eine Klasse X habe, und in einer Funktion folgendes schreibe:
1
Xx(100);
wird zuerst automatischer (lokaler) Speicher für das Objekt reserviert,
dann wird sein Konstruktor aufgerufen und diesem der Wert 100 übergeben.
Wenn ich stattdessen folgendes schreibe:
1
X*x=newX(100);
passiert quasi das gleiche, bis auf daß der Speicher dynamisch über den
Operator new reserviert wird.
Bei der Erzeugung eines Objekts, dessen Typ einen Konstruktor hat, wird
dieser immer implizit aufgerufen. Das kann man nicht verhindern, und man
kann man ihn auch nicht explizit aufrufen.
Rolf Magnus schrieb:> Wenn ich eine Klasse X habe, und in einer Funktion folgendes schreibe:> X x(100);
Das Beispiel hatte ich bis heute früh immer ignoriert (Weil selten bis
gar nicht von mir so angewendet). Und deswegen die
Verwechslung/Zuordnung/Zusammenfassung/... von 'new' als Schlüsselwort
und Operator. Als mir's dann einfiel, hatte ich auch den Gedanken, dass
es so funktionieren müsste, wie
Nico Sch. schrieb:> Genau diese Speicherholfunktion fehlt im avr-g++; den Ablauf, ein Objekt> in C++ zu erzeugen, kennt der Compiler jedoch.
Klaus Wachtler schrieb:> Der andere Punkt ist eben: woher nimmt man den Speicher?> Je nach System ist vielleicht nicht der ganze Speicher gleichwertig. Bei> manchen AVR kann man z.B. zwischen schnellerem internen und ggf.> externem Speicher wählen, oder auf anderen Systemen mit shared memory> oder memory mapped files spielen. Das wird ermöglicht, indem man> operator new umschreibt.
Eine andere Möglichkeit, wozu so etwas gerne benutzt wird, sind Debug
Hilfen in Form von Leak Detektoren.
Der op new führt eine Liste aller Allokierungen mit, der op delete
entfernt dann freigegebene wieder. Wenn am Programmende etwas in der
Liste übrig bleibt, dann hat man Memory Leaks.
Gerne wird das ganze dann auch mit Guard Bytes gekoppelt. Da allokiert
der op new einfach etwas mehr Speicher als angefordert, platziert den
angeforderten Speicher in die Mitte hinein und füllt die restlichen
Bytes mit einem bekannten Bytemuster auf. Der op delete prüft bei der
Freigabe, ob diese Guard Bytes noch das bekannte Muster aufweisen. Wenn
nicht ... dann hat man meistens auf ein Array Out Of Bounds geschrieben.
Jetzt, wo ich weiß worum es geht :-) könnte man ja auf einem AVR
speicherplatzsparend bei new/delete vielleicht sogar auf malloc()/free()
verzichten und sich mit dem Operator(!) ;-) 'new' nur einen Pointer auf
einen vorher reservierten Speicherbereich holen, den sich bestimmte
Objekte (die natürlich nicht gleichzeitig existieren dürfen) teilen.
-> über 500Byte Flash und einige Byte SRAM gespart.
Hallo Marcus,
anbei ein kleines C++-Programm für den ATmega128 (Nibo).
Es ist mein erster Test in C++ und ich bin froh, dass es funktioniert.
Hoffe es hilft Dir weiter :-)
Möchte mehr über C++ auf AVR lernen und mich austauschen.
Gruß
Michael