In C(++) sind Felder von Strukturen bekannt und üblich, wie im Beispiel:
typedef struct Data_t { int a; float f; }Data; Data* dataArray1 = new Data[12]; //(C++) Data* dataArray2 = (Data*)(malloc(12*sizeof(Data)); //C-Variante Data dataArray3[12]; //direkt angelegt mit fester Größe |
In Java sind dagegen alle Feldelemente, die keine einfache skalare Variable darstellen (int, float, ...), grundsätzlich über Referenzen geführt.
final class Data { int a; float f; } Data[] dataArray = new Data[12]; |
In dem gezeigtem Beispiel wird nicht etwa wie in C(++) ein Feld mit 12 Elementen vom Typ Data angelegt, sondern es wird nur ein Feld mit 12 nichtinitialisierten Referenzen auf den Typ Data angelegt. Würde Data nicht final sein, so könnten dort auch Instanzen referenziert werden, die von Data abgeleitetet sind. Um die Feldelemente tatsächlich anzulegen, ist nochmal Arbeit erforderlich:
for(int i = 0; i < dataArray.length(); i++){ dataArray[i] = new Data(); } |
Erst damit bekommt man das in C erwartete Ergebnis. Java ist hier etwas komplizierter, aber dafür universell und sicher. Die Verfahrensweise entspricht dem allgemein in Java verwendeten Referenzkonzept. Java kennt keine "eingebetteten Daten", alle Daten werden grundsätzlich referenziert. Damit ist es immer möglich, keine Daten zu haben (null-Zeiger) oder Daten in Instanzen aus abgeleiteten Klassen, ohne dass das irgendwo besonders zu beachten ist:
class Example { int val; Data data; } |
entspricht in C(++)
struct Example { int val; Data* data; //referenzierte Daten } |
und nicht
struct Example { int val; Data data; //eingebettete Daten, in Java nicht möglich. } |
Es ist die Frage zu stellen: Sind eingebettete Daten in Feldelementen in C(++) nötig, oder kann man per Festlegung des Programmierstils mit der Variante leben, die Java entspricht. Sie kann eindeutig zugunsten der Notwendigkeit von eigebetteten Daten beantwortet werden. Aus folgenden Gründen:
C und C++ sind wesentlich näher am Maschinencode als Java. Im Grunde spielt es für die Programmabarbeitung keine Rolle, ob Daten referenziert oder eingebettet sind. Für die Programmbearbeitung ist die referenzierte Variante die universellere und damit in Java die Wahl. Aber wenn die Daten aus anderen Gründen in einer bestimmten Form dargestellt werden sollen, hier ist auch an Hardwareanschaltungen gedacht, dann muss C(++) dies unterstützen.
In Java spielt die Frage: " eine Referenz (indirekter Zugriff) mehr oder weniger" - kaum eine Rolle. Es geht um wenige Nanosekunden für den zusätzlichen indirekte Zugriff und um wenige Bytes im Gigabyte-Speicherraum. C(++) wird dagegen häufig auch bei kleineren Speicherausbau eingesetzt. Es ist dann schon ein Unterschied, ob ein Array mit der oben dargestellten Struktur bei 100000 Elementen mit 8 Byte pro Element insgesamt 800000 Bytes (ca. 1 MByte) braucht, oder aufgrund der Referenzierung plus dem Organisationsaufwand für jedes extra zu allokierende Feldelement stattdessen in der Größenordnung einiger Megabyte. Für die Referenzen selbst würden im Beispiel schon 400000 Byte benötigt, jedes Feldelement benötigt Overhead für die Speicherallokierung.
Unter folgenden Umständen kann eine Java-Struktur in C(++) quellcodekompatibel mit eingebetteten Daten abgebildet werden:
Die Klasse der einzubettenten Daten ist final
, es ist also in Java nicht möglich, Instanzen einer abgeleiteten Klasse referenzieren zu wollen.
In Java kann man von einem Array die Länge abfragen. Außerdem führt Java eine Exception aus, wenn mit falschem Index zugegriffen wird. Diese beiden Punkte sind Schwachpunkte in C. In C++ hat man mit den Templates und der Template-Library versucht, eine Abhilfe zu schaffen. Jedoch zeigt die Tatsache, dass oft auch in C++ Arrays wie in C angelegt werden, dass diese Template-Library nicht die Ideallösung ist.
Hier kann eine Herangehensweise aus Java nach C übertragen eine gute Abhilfe schaffen. Danach wird ein Feld in C(++) wie folgt definiert:
typedef struct DataArray_t { ARRAYHEAD_Jc Data data[100]; }DataArray; |
ARRAYHEAD_Jc
ist ein Makro und stellt eine Basisklasse Object_Jc, einen Eintragsplatz für die Anzahl der Feldelemente, einen Eintragsplatz für die Größe jedes Feldelementes und einige Mode-Bits bereit. Insgesamt werden für diesen Kopf des Feldes 16 Bytes benötigt (8 Bytes für Object_Jc).
Auf den Kopf folgen die Daten, und zwar unmittelbar. Die feste Angabe einer Elementanzahl (hier [100]
) ist für die C-Syntax notwendig, es ist aber zur Laufzeit nicht bindend, dass es genau 100 Elemente sein müssen. Zur Laufzeit kann bei einer nichtstatischen Anlage der Daten auch für mehr oder weniger Elemente Speicher bereitgestellt werden. Die tatsächliche Elementeanzahl ist in den Kopfdaten gespeichert. Syntaktisch richtig wäre es auch, nur 1 Element zu reservieren. Die Angabe einer größeren Anzahl stellt eine Debughilfe dar. Für den Debugger, zum Beispiel von Visual Studio, ist eine Elementeanzahl angegeben, die unmittelbar betrachtet werden kann.
Für das Feld muss noch ein Konstruktor und eine new-Methode definiert werden. Der Konstruktor ist als Funktionsprototyp zu deklarieren und in einem C(pp)-File auszuprogrammieren. Im Konstruktor sind die Kopfdaten zu füllen. Sinnvollerweise können im Konstruktor auch die Datenelemente geeignet belegt werden.
Die new-Methode kann dagegen als define ausgeführt werden:
/** Constructs the array */ METHOD_C DataArray* constructor_DataArray(DataArray* ythis, int size); #define new_DataArray(SIZE) (constructor_DataArray( (DataArray*)malloc(sizeof_ARRAY_Jc(Data, SIZE )), SIZE)) |
Die new-Konstruktion kann ähnlich dem new in C++ oder Java verwendet werden und legt ein Feld im Heap komplett an. Die Byteanzahl wird mit einem Makro ermittelt, das in Object_Jc.h definiert ist. Für dieses new-Konstrukt gibt der Konstruktor als Returnwert den ythis-Zeiger zurück, sonst wäre die Makro-Schreibweise nicht möglich (es würde eine Zwischenvariable benötigt werden). Das ist aber laufzeittechnisch problemlos.
Der Konstruktor kann in etwa wie folgt aussehen:
METHOD_C DataArray* constructor_DataArray(DataArray* ythis, int size) { constructor_ARRAY_Jc(ythis, size, sizeof(Data), &reflection__DataArray.reflection); memset(ythis->data, 0, size * ythis->sizeElement); return ythis; } |
Dabei wird der constructor_ARRAY_Jc()
gerufen, um die Kopfdaten einschließlich der Object_Jc-Daten zu initialisieren (Reflection-Infos).
Die Anwendung ist dann recht einfach. Im folgenden Beispiel werden mehrere Zugriffe gezeigt. Die Varianten mit Angabe des Index in eckigen Klammern arbeiten ohne Indextest. Darunter stehen jeweils adäquate Befehle mit Indextest. Diese bauen auf ein Makro element_ARRAY_Jc auf.
DataArray* array = new_DataArray(25); array->data[11].a = 22; element_ARRAY_Jc(array, 12)->a = 23; { int ii; for(ii=0; ii < arrayB->length; ii++) { arrayB->data[11].b = ii+5; } } { int a = array->data[11].a; a = element_ARRAY_Jc(array, 12)->a; } |
Die adäquate Java-Schreibweise wäre:
DataArray[] array = new DataArray[25]; array[11].a = 22; array[12].a = 23; { int ii; for(ii=0; ii < arrayB.length; ii++) { arrayB[11].b = ii+5; } } { int a = array[11].a; a = array[12].a; } |
Traditionell in C wäre zu programmieren:
int lenghtArray; lengthArray = 25; Data* array = (Data*)malloc(25 * sizeof(Data)); array[11].a = 22; if(12 < lengthArray) array[12].a = 23; { int ii; for(ii=0; ii < lengthArray; ii++) { arrayB[11].b = ii+5; } } { int a = array[11].a; a = (12 < lengthArray ? array[12].a : 0); } |
Hier ist die Länge des Feldes manuell zu verwalten. Es gibt keine Reflections. Ist ein Indextest notwendig, dann muss der Anwender überlegen, war er dann tut. Im obigen Beispiel wird anstelle des Feldzugriffes eine 0 gelesen. Damit wird aber der Fehler verschleiert. Richtig ist es, eine Ausnahmebehandlung auszuführen, wie im Makro element_ARRAY_Jc()
realisiert.