Wie nützlich ist Cs "wahre" Größenbestimmung von Variablen?

9

Eine Sache, die mich immer intuitiv als positives Merkmal von C empfunden hat (nun ja, tatsächlich von seinen Implementierungen wie gcc, clang, ...), ist die Tatsache, dass es zur Laufzeit keine versteckten Informationen neben Ihren eigenen Variablen speichert. Damit meine ich, wenn Sie zum Beispiel eine Variable "x" vom Typ "uint16_t" wollten, könnten Sie sicher sein, dass "x" nur 2 Bytes Speicherplatz belegt (und keine versteckten Informationen wie den Typ usw. Enthält .). Wenn Sie ein Array mit 100 Ganzzahlen wünschen, können Sie sicher sein, dass es 100 Ganzzahlen groß ist.

Je mehr ich jedoch versuche, konkrete Anwendungsfälle für diese Funktion zu entwickeln, desto mehr frage ich mich, ob sie überhaupt praktische Vorteile hat. Das einzige, was ich mir bisher einfallen lassen konnte, ist, dass es offensichtlich weniger RAM benötigt. Für begrenzte Umgebungen wie AVR-Chips usw. ist dies definitiv ein großes Plus, aber für alltägliche Desktop- / Server-Anwendungsfälle scheint es ziemlich irrelevant zu sein. Eine andere Möglichkeit , die ich denke, dass es vielleicht hilfreich sein / entscheidend für den Zugriff auf Hardware, oder vielleicht Speicherbereiche abbildet (zB für VGA - Ausgang und dergleichen) ...?

Meine Frage: Gibt es konkrete Domänen, die ohne diese Funktion nicht oder nur sehr umständlich implementiert werden können?

PS Bitte sag mir, ob du einen besseren Namen dafür hast! ;)

Thomas Oltmann
quelle
@gnat Ich glaube ich verstehe was dein Problem ist. Es ist, weil es mehrere Antworten geben könnte, oder? Nun, ich verstehe, dass diese Frage möglicherweise nicht zu der Funktionsweise von Stackexchange passt, aber ich weiß ehrlich gesagt nicht, wo ich sie sonst stellen soll ...
Thomas Oltmann
1
@lxrec RTTI wird in der vtable gespeichert, und Objekte speichern nur einen Zeiger auf die vtable. Darüber hinaus verfügen Typen nur über RTTI, wenn sie bereits über eine vtable verfügen, da sie über eine virtualMember-Funktion verfügen . RTTI vergrößert also niemals Objekte, sondern vergrößert die Binärdatei nur um eine Konstante.
3
@ThomasOltmann Jedes Objekt mit virtuellen Methoden benötigt einen vtable-Zeiger. Ohne das können Sie die virtuellen Methoden der Funktionalität nicht haben. Darüber hinaus entscheiden Sie sich ausdrücklich für virtuelle Methoden (und damit für eine vtable).
1
@ ThomasOltmann Du scheinst sehr verwirrt zu sein. Es ist kein Zeiger auf ein Objekt, das einen vtable-Zeiger enthält, sondern das Objekt selbst. Dh T *ist immer gleich groß und Tkann ein verstecktes Feld enthalten, das auf die vtable zeigt. Und kein C ++ - Compiler hat jemals vtables in Objekte eingefügt, die sie nicht benötigen.

Antworten:

5

Es gibt mehrere Vorteile, der offensichtliche liegt bei der Kompilierung, um sicherzustellen, dass Dinge wie Funktionsparameter mit den übergebenen Werten übereinstimmen.

Aber ich denke, Sie fragen, was zur Laufzeit passiert.

Beachten Sie, dass der Compiler eine Laufzeit erstellt, die das Wissen über die Datentypen in die von ihm ausgeführten Vorgänge einbettet. Jeder Datenblock im Speicher ist möglicherweise nicht selbstbeschreibend, aber der Code weiß von Natur aus, was diese Daten sind (wenn Sie Ihre Arbeit korrekt ausgeführt haben).

Zur Laufzeit sind die Dinge etwas anders als Sie denken.

Nehmen Sie beispielsweise nicht an, dass nur zwei Bytes verwendet werden, wenn Sie uint16_t deklarieren. Je nach Prozessor und Wortausrichtung kann es 16, 32 oder 64 Bit auf dem Stapel belegen. Möglicherweise verbraucht Ihr Shorts-Array viel mehr Speicher als erwartet.

Dies kann in bestimmten Situationen problematisch sein, in denen Sie Daten an bestimmten Offsets referenzieren müssen. Dies geschieht bei der Kommunikation zwischen zwei Systemen mit unterschiedlichen Prozessorarchitekturen, entweder über eine drahtlose Verbindung oder über Dateien.

Mit C können Sie Strukturen mit Granularität auf Bitebene angeben:

struct myMessage {
  uint8_t   first_bit: 1;
  uint8_t   second_bit: 1;
  uint8_t   padding:6;
  uint16_t  somethingUseful;
}

Diese Struktur ist drei Byte lang, wobei ein Kurzschluss definiert ist, um mit einem ungeraden Versatz zu beginnen. Es muss auch verpackt werden, um genau so zu sein, wie Sie es definiert haben. Andernfalls richtet der Compiler die Elemente in Wortausrichtung aus.

Der Compiler generiert Code hinter den Kulissen, um diese Daten zu extrahieren und in ein Register zu kopieren, damit Sie nützliche Dinge damit tun können.

Jetzt können Sie sehen, dass mein Programm jedes Mal, wenn es auf ein Mitglied der myMessage-Struktur zugreift, weiß, wie es genau extrahiert und bearbeitet wird.

Dies kann problematisch und schwierig zu handhaben sein, wenn zwischen verschiedenen Systemen mit verschiedenen Softwareversionen kommuniziert wird. Sie müssen das System und den Code sorgfältig entwerfen, um sicherzustellen, dass beide Seiten genau die gleiche Definition der Datentypen haben. Dies kann in einigen Umgebungen eine große Herausforderung sein. Hier benötigen Sie ein besseres Protokoll, das selbstbeschreibende Daten wie die Protokollpuffer von Google enthält .

Zuletzt sollten Sie sich fragen, wie wichtig dies in der Desktop- / Serverumgebung ist. Es hängt wirklich davon ab, wie viel Speicher Sie verwenden möchten. Wenn Sie beispielsweise eine Bildverarbeitung ausführen, wird möglicherweise viel Speicher benötigt, was sich auf die Leistung Ihrer Anwendung auswirken kann. Dies ist definitiv immer ein Problem in der eingebetteten Umgebung, in der der Speicher eingeschränkt ist und kein virtueller Speicher vorhanden ist.

Tereus Scott
quelle
2
"Möglicherweise verbraucht Ihre Shorts viel mehr Speicher als erwartet." Dies ist in C falsch: Arrays enthalten ihre Elemente garantiert lückenlos. Ja, das Array muss richtig ausgerichtet sein, genau wie ein einzelnes short. Dies ist jedoch eine einmalige Voraussetzung für den Start des Arrays. Der Rest wird automatisch korrekt ausgerichtet, da er aufeinanderfolgend ist.
cmaster - wieder
Auch die Syntax für das Auffüllen ist falsch uint8_t padding: 6;, genau wie bei den ersten beiden Bits. Oder klarer nur der Kommentar //6 bits of padding inserted by the compiler. Die Struktur hat, wie Sie sie geschrieben haben, eine Größe von mindestens neun Bytes, nicht drei.
cmaster - wieder Monica
9

Sie treffen auf einen der einzigen Gründe, warum dies nützlich ist: die Zuordnung externer Datenstrukturen. Dazu gehören speicherabgebildete Videopuffer, Hardwareregister usw. Dazu gehören auch Daten, die intakt außerhalb des Programms übertragen werden, wie SSL-Zertifikate, IP-Pakete, JPEG-Bilder und so ziemlich jede andere Datenstruktur, die außerhalb des Programms eine dauerhafte Lebensdauer hat.

Ross Patterson
quelle
5

C ist eine einfache Sprache, fast ein portabler Assembler, daher befinden sich die Datenstrukturen und Sprachkonstrukte in der Nähe des Metalls (Datenstrukturen verursachen keine versteckten Kosten - mit Ausnahme der durch Hardware und ABI auferlegten Einschränkungen hinsichtlich Auffüllen, Ausrichtung und Größe ). C hat also in der Tat keine dynamische Typisierung von Haus aus. Wenn Sie es jedoch benötigen, können Sie eine Konvention festlegen, dass alle Ihre Werte Aggregate sind, beginnend mit bestimmten Typinformationen (z. B. einigen ...). Verwendung -s und (für arrayartige Dinge) flexible Anordnungs - Element in sich auch die Größe des Arrays enthält.enumunionstruct

(Wenn Sie in C programmieren, liegt es in Ihrer Verantwortung, nützliche Konventionen zu definieren, zu dokumentieren und zu befolgen - insbesondere Vor- und Nachbedingungen und Invarianten. Auch die dynamische Speicherzuweisung von C erfordert explizite Konventionen darüber, wer freeeine gehäufte mallocSpeicherzone verwenden soll.)

Also, Werte zu repräsentieren , die boxed ganze Zahlen sind, oder Strings, oder irgendeine Art von Schema -ähnlichen Symbol oder Vektoren von Werten, werden Sie das Konzept eine verwenden getaggten Vereinigung (als Vereinigung von Zeigern implementiert) -Immer vom Typ Art Start -, z.B:

enum value_kind_en {V_NONE, V_INT, V_STRING, V_SYMBOL, V_VECTOR};
union value_en { // this union takes a word in memory
   const void* vptr; // generic pointer, e.g. to free it
   enum value_kind_en* vkind; // the value of *vkind decides which member to use
   struct intvalue_st* vint;
   struct strvalue_st* vstr;
   struct symbvalue_st* vsymb;
   struct vectvalue_st* vvect;
};
typedef union value_en value_t;
#define NULL_VALUE  ((value_t){NULL})
struct intvalue_st {
  enum value_kind_en kind; // always V_INT for intvalue_st
  int num;
};
struct strvalue_st {
  enum value_kind_en kind; // always V_STRING for strvalue_st
  const char*str;
};
struct symbvalue_st {
  enum value_kind_en kind; // V_SYMBOL
  struct strvalue_st* symbname;
  value_t symbvalue;
};
struct vectvalue_st {
  enum value_kind_en kind; // V_VECTOR;
  unsigned veclength;
  value_t veccomp[]; // flexible array of veclength components.
};

Um den dynamischen Typ eines Wertes zu erhalten

enum value_kind_en value_type(value_t v) {
  if (v.vptr != NULL) return *(v.vkind);
  else return V_NONE;
}

Hier ist eine "dynamische Umwandlung" in Vektoren:

struct vectvalue_st* dyncast_vector (value_t v) {
   if (value_type(v) == V_VECTOR) return v->vvect;
   else return NULL;
}

und ein "sicherer Accessor" innerhalb von Vektoren:

value_t vector_nth(value_t v, unsigned rk) {
   struct vectvalue_st* vecp = dyncast_vector(v);
   if (vecp && rk < vecp->veclength) return vecp->veccomp[rk];
   else return NULL_VALUE;
}

Normalerweise definieren Sie die meisten der oben genannten static inlineKurzfunktionen wie in einer Header-Datei.

Übrigens, wenn Sie den Garbage Collector von Boehm verwenden können, können Sie ganz einfach in einem übergeordneten (aber unsicheren) Stil codieren, und mehrere Scheme-Interpreter werden auf diese Weise ausgeführt. Ein variadischer Vektorkonstruktor könnte sein

value_t make_vector(unsigned size, ... /*value_t arguments*/) {
   struct vectvalue_st* vec = GC_MALLOC(sizeof(*vec)+size*sizeof(value));
   vec->kind = V_VECTOR;
   va_args args;
   va_start (args, size);
   for (unsigned ix=0; ix<size; ix++) 
     vec->veccomp[ix] = va_arg(args,value_t);
   va_end (args);
   return (value_t){vec};
}

und wenn Sie drei Variablen haben

value_t v1 = somevalue(), v2 = otherval(), v3 = NULL_VALUE;

Sie können mit ihnen einen Vektor erstellen make_vector(3,v1,v2,v3)

Wenn Sie Böhms Garbage Collector nicht verwenden (oder Ihren eigenen entwerfen) möchten, sollten Sie sehr vorsichtig sein, um Destruktoren zu definieren und zu dokumentieren, wer, wie und wann Speicher free-d sein sollte. siehe dieses Beispiel. Sie könnten also mallocanstelle von GC_MALLOCoben verwenden (aber dann auf seinen Fehler testen), aber Sie müssen einige Destruktorfunktionen sorgfältig definieren und verwendenvoid destroy_value(value_t)

Die Stärke von C besteht darin, niedrig genug zu sein, um Code wie oben zu ermöglichen und Ihre eigenen Konventionen (insbesondere für Ihre Software) zu definieren.

Basile Starynkevitch
quelle
Ich denke, Sie haben meine Frage falsch verstanden. Ich möchte keine dynamische Eingabe in C. Ich war neugierig, ob diese spezifische Eigenschaft von C von praktischem Nutzen ist.
Thomas Oltmann
Aber auf welche genaue Eigenschaft von C beziehen Sie sich? C-Datenstrukturen befinden sich in der Nähe des Metalls, haben also keine versteckten Kosten (außer Ausrichtung und Größenbeschränkungen)
Basile Starynkevitch
Genau das: /
Thomas Oltmann
C wurde als Low-Level-Sprache erfunden, aber wenn Optimierungen für Compiler wie gcc aktiviert sind, wird eine Sprache verarbeitet, die die Low-Level-Syntax verwendet, aber keinen zuverlässigen Low-Level-Zugriff auf plattformbasierte Verhaltensgarantien bietet. Man braucht sizeof, um malloc und memcpy zu verwenden, aber die Verwendung für schickere Adressberechnungen wird in "modernem" C möglicherweise nicht unterstützt
Supercat