size_t vs. uintptr_t

246

Der C-Standard garantiert, dass dieser size_tTyp jeden Array-Index enthalten kann. Dies bedeutet, dass logischerweise size_tjeder Zeigertyp enthalten sein sollte. Ich habe auf einigen Websites, die ich auf den Googles gefunden habe, gelesen, dass dies legal ist und / oder immer funktionieren sollte:

void *v = malloc(10);
size_t s = (size_t) v;

In C99 führte der Standard die Typen intptr_tund uintptr_tein, die signierte und nicht signierte Typen sind, die garantiert Zeiger enthalten können:

uintptr_t p = (size_t) v;

Was ist der Unterschied zwischen size_tund uintptr_t? Beide sind nicht signiert und sollten in der Lage sein, jeden Zeigertyp zu halten, sodass sie funktional identisch zu sein scheinen. Gibt es einen wirklich zwingenden Grund, a uintptr_t(oder besser noch void *) a anstelle von a zu verwenden size_t, außer Klarheit? Gibt es in einer undurchsichtigen Struktur, in der das Feld nur von internen Funktionen behandelt wird, einen Grund, dies nicht zu tun?

Aus dem gleichen Grund war ptrdiff_tes ein vorzeichenbehafteter Typ, der Zeigerunterschiede aufnehmen kann und daher fast jeden Zeiger aufnehmen kann. Wie unterscheidet er sich also von intptr_t?

Sind nicht alle diese Typen grundsätzlich trivial unterschiedliche Versionen derselben Funktion? Wenn nicht, warum? Was kann ich mit einem von ihnen nicht machen, was ich mit einem anderen nicht machen kann? Wenn ja, warum hat C99 der Sprache zwei im Wesentlichen überflüssige Typen hinzugefügt?

Ich bin bereit, Funktionszeiger zu ignorieren, da sie nicht für das aktuelle Problem gelten, aber ich kann sie gerne erwähnen, da ich den Verdacht habe, dass sie für die "richtige" Antwort von zentraler Bedeutung sind.

Chris Lutz
quelle

Antworten:

236

size_tist ein Typ, der jeden Array-Index enthalten kann. Dies bedeutet, dass size_t logischerweise jeden Zeigertyp enthalten kann

Nicht unbedingt! Erinnern Sie sich beispielsweise an die Zeit segmentierter 16-Bit-Architekturen: Ein Array ist möglicherweise auf ein einzelnes Segment beschränkt (ein 16-Bit-Array size_twürde dies tun), ABER Sie könnten mehrere Segmente haben ( intptr_tfür die Auswahl wäre also ein 32-Bit- Typ erforderlich das Segment sowie den Versatz darin). Ich weiß, dass diese Dinge in diesen Tagen von einheitlich adressierbaren, nicht segmentierten Architekturen seltsam klingen, aber der Standard MUSS eine größere Vielfalt als "was 2009 normal ist" berücksichtigen, wissen Sie! -)

Alex Martelli
quelle
6
Dies erklärt zusammen mit den zahlreichen anderen, die zu dem gleichen Schluss gekommen sind, den Unterschied zwischen size_tund, uintptr_taber was ist mit ptrdiff_tund intptr_t- könnten nicht beide den gleichen Wertebereich auf fast jeder Plattform speichern? Warum haben sowohl vorzeichenbehaftete als auch vorzeichenlose Ganzzahltypen in Zeigergröße, insbesondere wenn sie ptrdiff_tbereits den Zweck eines Ganzzahlentyps in Vorzeichengröße mit Vorzeichen erfüllen?
Chris Lutz
8
Der Schlüsselbegriff lautet "auf fast jeder Plattform", @Chris. Eine Implementierung kann Zeiger auf den Bereich 0xf000-0xffff beschränken - dies erfordert ein 16-Bit-intptr_t, aber nur ein 12/13-Bit-ptrdiff_t.
Paxdiablo
29
@Chris, nur für Zeiger innerhalb desselben Arrays ist es genau definiert, ihren Unterschied zu berücksichtigen. Auf genau denselben segmentierten 16-Bit-Architekturen (Array muss in einem einzelnen Segment leben, aber zwei verschiedene Arrays können sich in unterschiedlichen Segmenten befinden) müssen Zeiger 4 Byte groß sein, Zeigerunterschiede können jedoch 2 Byte betragen!
Alex Martelli
6
@AlexMartelli: Nur dass Zeigerunterschiede positiv oder negativ sein können. Der Standard muss size_tmindestens 16 Bit, aber ptrdiff_tmindestens 17 Bit betragen (was in der Praxis bedeutet, dass es wahrscheinlich mindestens 32 Bit sein wird).
Keith Thompson
3
Egal, segmentierte Architekturen, was ist mit einer modernen Architektur wie x86-64? Frühe Implementierungen dieser Architektur geben Ihnen nur einen adressierbaren 48-Bit-Raum, aber die Zeiger selbst sind ein 64-Bit-Datentyp. Der größte zusammenhängende Speicherblock, den Sie vernünftigerweise adressieren könnten, wäre 48-Bit, also muss ich mir vorstellen, SIZE_MAXdass er nicht 2 ** 64 sein sollte. Dies verwendet eine flache Adressierung, wohlgemerkt; Es ist keine Segmentierung erforderlich, um eine Nichtübereinstimmung zwischen SIZE_MAXund dem Bereich eines Datenzeigers zu haben.
Andon M. Coleman
89

Zu Ihrer Aussage:

"Der C-Standard garantiert, dass dieser size_tTyp einen beliebigen Array-Index enthalten kann. Dies bedeutet, dass logischerweise size_tjeder Zeigertyp gespeichert werden kann."

Dies ist tatsächlich ein Irrtum (ein Missverständnis, das sich aus falschen Überlegungen ergibt) (a) . Sie können denken Letzteres aus Ersterem folgt, aber das ist eigentlich nicht der Fall.

Zeiger und Array-Indizes sind es nicht dasselbe. Es ist durchaus plausibel, sich eine konforme Implementierung vorzustellen, die Arrays auf 65536 Elemente beschränkt, es Zeigern jedoch ermöglicht, jeden Wert in einem massiven 128-Bit-Adressraum zu adressieren.

C99 gibt an, dass die Obergrenze einer size_tVariablen durch definiert istSIZE_MAX und diese bis zu 65535 betragen kann (siehe C99 TR3, 7.18.3, unverändert in C11). Zeiger wären ziemlich begrenzt, wenn sie in modernen Systemen auf diesen Bereich beschränkt wären.

In der Praxis werden Sie wahrscheinlich feststellen, dass Ihre Annahme zutrifft, aber das liegt nicht daran, dass der Standard dies garantiert. Weil es das eigentlich nicht garantiert.


(a) Dies ist übrigens keine Form von persönlichem Angriff, sondern nur die Begründung, warum Ihre Aussagen im Kontext des kritischen Denkens falsch sind. Zum Beispiel ist die folgende Argumentation ebenfalls ungültig:

Alle Welpen sind süß. Dieses Ding ist süß. Deshalb muss dieses Ding ein Welpe sein.

Die Niedlichkeit oder das Gegenteil von Welpen spielt hier keine Rolle. Ich sage nur, dass die beiden Tatsachen nicht zu einer Schlussfolgerung führen, da die ersten beiden Sätze die Existenz von niedlichen Dingen zulassen, die es nicht sind Welpen sind.

Dies ähnelt Ihrer ersten Aussage, die nicht unbedingt die zweite vorschreibt.

paxdiablo
quelle
Anstatt das, was ich in den Kommentaren für Alex Martelli gesagt habe, erneut einzugeben, möchte ich mich nur für die Klarstellung bedanken, aber die zweite Hälfte meiner Frage (den ptrdiff_tvs.- intptr_tTeil) wiederholen .
Chris Lutz
5
@Ivan, wie bei den meisten Kommunikationen, muss ein gemeinsames Verständnis für bestimmte grundlegende Elemente vorhanden sein. Wenn Sie diese Antwort als "Spaß machen" ansehen, versichere ich Ihnen, dass dies ein Missverständnis meiner Absicht ist. Angenommen, Sie beziehen sich auf meinen Kommentar zum „logischen Irrtum“ (ich sehe keine andere Möglichkeit), war dies als sachliche Aussage gedacht, nicht als Aussage, die auf Kosten des OP gemacht wurde. Wenn Sie eine konkrete Verbesserung vorschlagen möchten, um die Möglichkeit von Missverständnissen zu minimieren (und nicht nur eine allgemeine Beschwerde), würde ich gerne darüber nachdenken.
Paxdiablo
1
@ivan_pozdeev - das ist ein widerliches und drastisches Paar von Änderungen, und ich sehe keine Beweise dafür, dass paxdiablo sich über irgendjemanden "lustig gemacht" hat. Wenn ich der OP wäre, würde ich das gleich zurückrollen ...
ex nihilo
1
@Ivan, war mit den von Ihnen vorgeschlagenen Änderungen nicht wirklich zufrieden, hat einen Rollback durchgeführt und auch versucht, unbeabsichtigte Verstöße zu beseitigen. Wenn Sie weitere Änderungen anbieten möchten, würde ich vorschlagen, einen Chat zu starten, damit wir darüber diskutieren können.
Paxdiablo
1
@paxdiablo okay, ich denke "das ist eigentlich ein Irrtum" ist weniger bevormundend.
ivan_pozdeev
36

Ich werde alle anderen Antworten in Bezug auf die Argumentation mit Segmentbeschränkungen, exotischen Architekturen usw. für sich stehen lassen.

Ist der einfache Unterschied in den Namen nicht Grund genug, den richtigen Typ für das Richtige zu verwenden?

Wenn Sie eine Größe speichern, verwenden Sie size_t. Wenn Sie einen Zeiger speichern, verwenden Sie intptr_t. Eine Person, die Ihren Code liest, wird sofort wissen, dass "aha, dies ist eine Größe von etwas, wahrscheinlich in Bytes" und "oh, hier ist ein Zeigerwert, der aus irgendeinem Grund als Ganzzahl gespeichert wird".

Ansonsten könnte man einfach unsigned long(oder in diesen hier modernen Zeiten unsigned long long) für alles verwenden. Größe ist nicht alles, Typnamen haben eine Bedeutung, die nützlich ist, da sie zur Beschreibung des Programms beitragen.

entspannen
quelle
Ich stimme zu, aber ich dachte über einen Hack / Trick nach (den ich natürlich klar dokumentieren würde), bei dem ein Zeigertyp in einem size_tFeld gespeichert wird .
Chris Lutz
@MarkAdler Standard erfordert nicht, dass Zeiger insgesamt als Ganzzahlen dargestellt werden können: Jeder Zeigertyp kann in einen Ganzzahltyp konvertiert werden. Sofern nicht anders angegeben, ist das Ergebnis implementierungsdefiniert. Wenn das Ergebnis nicht im Integer-Typ dargestellt werden kann, ist das Verhalten undefiniert. Das Ergebnis muss nicht im Wertebereich eines Ganzzahltyps liegen. Somit ist nur void*, intptr_tund uintptr_tsind garantiert alle Zeiger auf Daten darstellen zu können.
Andrew Svietlichnyy
12

Möglicherweise ist die Größe des größten Arrays kleiner als ein Zeiger. Denken Sie an segmentierte Architekturen - Zeiger können 32-Bit sein, aber ein einzelnes Segment kann möglicherweise nur 64 KB adressieren (zum Beispiel die alte 8086-Architektur im Real-Modus).

Während diese auf Desktop-Computern nicht mehr häufig verwendet werden, soll der C-Standard auch kleine, spezialisierte Architekturen unterstützen. Es gibt immer noch eingebettete Systeme, die beispielsweise mit 8- oder 16-Bit-CPUs entwickelt werden.

Michael Burr
quelle
Aber Sie können Zeiger genau wie Arrays indizieren, sollten Sie also size_tauch damit umgehen können? Oder würden sich dynamische Arrays in einem weit entfernten Segment immer noch auf die Indizierung innerhalb ihres Segments beschränken?
Chris Lutz
Das Indizieren von Zeigern wird technisch nur auf die Größe des Arrays unterstützt, auf das sie zeigen. Wenn ein Array auf eine Größe von 64 KB beschränkt ist, ist dies alles, was die Zeigerarithmetik unterstützen muss. MS-DOS-Compiler unterstützten jedoch ein "riesiges" Speichermodell, bei dem Fernzeiger (segmentierte 32-Bit-Zeiger) so manipuliert wurden, dass sie den gesamten Speicher als ein einziges Array adressieren konnten - aber die Arithmetik für Zeiger hinter den Kulissen war ziemlich hässlich - wenn der Versatz über einen Wert von 16 (oder etwas) hinaus erhöht wurde, wurde der Versatz auf 0 zurückgesetzt und der Segmentteil wurde erhöht.
Michael Burr
7
Lesen Sie en.wikipedia.org/wiki/C_memory_model#Memory_segmentation und weinen Sie um die verstorbenen MS-DOS-Programmierer, damit wir frei sind.
Justicle
Schlimmer war, dass die stdlib-Funktion sich nicht um das RIESIGE Schlüsselwort kümmerte. 16 - Bit - MS-C für alle strFunktionen und Borland auch für die memFunktionen ( memset, memcpy, memmove). Dies bedeutete, dass Sie einen Teil des Speichers überschreiben konnten, wenn der Offset überlief. Das Debuggen auf unserer eingebetteten Plattform hat Spaß gemacht.
Patrick Schlüter
@Justicle: Die segmentierte 8086-Architektur wird in C nicht gut unterstützt, aber ich kenne keine andere Architektur, die effizienter ist, wenn ein Adressraum von 1 MB ausreicht, ein 64-KB-Adressraum jedoch nicht. Einige moderne JVMs verwenden die Adressierung tatsächlich sehr ähnlich wie der x86-Real-Modus. Dabei werden 32-Bit-Objektreferenzen um 3 Bit nach links verschoben, um Objektbasisadressen in einem 32-GB-Adressraum zu generieren.
Supercat
5

Ich würde mir vorstellen (und das gilt für alle Typnamen), dass es Ihre Absichten im Code besser vermittelt.

Zum Beispiel, obwohl unsigned shortund wchar_tunter Windows die gleiche Größe haben (glaube ich), zeigt die Verwendung von wchar_tanstelle von unsigned shortdie Absicht, dass Sie damit ein breites Zeichen und nicht nur eine beliebige Zahl speichern.

Dreamlax
quelle
Aber hier gibt es einen Unterschied - auf meinem System wchar_tist es viel größer als ein unsigned shortsolches, so dass die Verwendung eines Systems für das andere fehlerhaft wäre und ein ernstes (und modernes) Portabilitätsproblem schafft, während das Portabilitätsproblem zwischen size_tund uintptr_tin den fernen Ländern zu liegen scheint von 1980-etwas (zufälliger Stich in die Dunkelheit am Datum, dort)
Chris Lutz
Touché! Aber andererseits size_tund haben uintptr_timmer noch Verwendungen in ihren Namen impliziert.
Dreamlax
Sie tun es, und ich wollte wissen, ob es dafür eine Motivation gibt, die über die Klarheit hinausgeht. Und es stellt sich heraus, dass es gibt.
Chris Lutz
3

Wenn ich sowohl vorwärts als auch rückwärts schaue und mich daran erinnere, dass verschiedene seltsame Architekturen über die Landschaft verstreut waren, bin ich mir ziemlich sicher, dass sie versucht haben, alle vorhandenen Systeme zu verpacken und auch alle möglichen zukünftigen Systeme bereitzustellen.

So sicher, wie sich die Dinge eingestellt haben, haben wir bisher nicht so viele Typen gebraucht.

Aber selbst in LP64, einem weit verbreiteten Paradigma, benötigten wir size_t und ssize_t für die Systemaufrufschnittstelle. Man kann sich ein eingeschränkteres Legacy- oder zukünftiges System vorstellen, bei dem die Verwendung eines vollständigen 64-Bit-Typs teuer ist und sie möglicherweise auf E / A-Operationen mit mehr als 4 GB zugreifen möchten, aber immer noch 64-Bit-Zeiger haben.

Ich denke, man muss sich fragen: Was könnte entwickelt worden sein, was könnte in Zukunft kommen. (Vielleicht internetweite 128-Bit-Zeiger für verteilte Systeme, aber nicht mehr als 64 Bit in einem Systemaufruf oder vielleicht sogar ein "Legacy" -32-Bit-Limit. :-) Stellen Sie sich vor, Legacy-Systeme könnten neue C-Compiler erhalten. .

Schauen Sie sich auch an, was es damals gab. Wie wäre es neben den zillion 286 Real-Mode-Speichermodellen mit den CDC-60-Bit-Wort- / 18-Bit-Zeiger-Mainframes? Wie wäre es mit der Cray-Serie? Egal, normales ILP64, LP64, LLP64. (Ich dachte immer, Microsoft sei mit LLP64 vorgetäuscht, es hätte P64 sein sollen.) Ich kann mir durchaus vorstellen, dass ein Komitee versucht, alle Grundlagen abzudecken ...

DigitalRoss
quelle
-9
int main(){
  int a[4]={0,1,5,3};
  int a0 = a[0];
  int a1 = *(a+1);
  int a2 = *(2+a);
  int a3 = 3[a];
  return a2;
}

Dies bedeutet, dass intptr_t immer size_t ersetzen muss und umgekehrt.

Chris Becke
quelle
10
Dies alles zeigt eine bestimmte Syntax-Eigenart von C. Die Array-Indizierung wird definiert als x [y], das * (x + y) entspricht, und da a + 3 und 3 + a in Typ und Wert identisch sind, können Sie dies tun benutze 3 [a] oder a [3].
Fred Nurk