size_t проти uintptr_t


246

Стандарт C гарантує, що size_tце тип, який може містити будь-який індекс масиву. Це означає, що, логічно, size_tмає бути можливість утримувати будь-який тип вказівника. Я читав на деяких сайтах, які з’ясував у Google, що це законно та / або завжди має працювати:

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

Тоді в C99 стандарт представив типи intptr_tта uintptr_tтипи, які підписують та підписують типи, гарантовано вміщуючи покажчики:

uintptr_t p = (size_t) v;

Тож у чому різниця між використанням size_tта uintptr_t? Обидва не підписані, і обидва повинні мати можливість утримувати будь-який тип вказівника, тому вони здаються функціонально однаковими. Чи є якась реальна вагома причина використовувати uintptr_t(а ще краще, а void *), а не size_tіншу, ніж ясність? Чи є в непрозорій структурі, де поле буде оброблятися лише внутрішніми функціями, чи немає цього?

Таким же чином, ptrdiff_tбув підписаний тип, здатний утримувати різницю покажчиків, а отже, здатний утримувати більшість будь-яких вказівників, тож чим його відрізняти intptr_t?

Чи не всі ці типи в основному обслуговують тривіально різні версії однієї функції? Якщо ні, то чому? Що я не можу зробити з одним із них, що я не можу зробити з іншим? Якщо так, то чому C99 додав до мови два по суті зайвих типи?

Я готовий ігнорувати функціональні покажчики, оскільки вони не стосуються поточної проблеми, але сміливо їх згадую, оскільки у мене є підозрілі припущення, що вони будуть центральними у "правильній" відповіді.

Відповіді:


236

size_tце тип, який може містити будь-який індекс масиву. Це означає, що, логічно, розмір_t повинен мати можливість утримувати будь-який тип вказівника

Не обов'язково! Скажімо, поверніться до часів сегментованої 16-бітної архітектури, наприклад: масив може бути обмежений одним сегментом (так size_tби зробив 16-розрядний ), Але у вас може бути декілька сегментів (тому intptr_tдля вибору потрібен 32-бітний тип відрізок, а також зміщення всередині нього). Я знаю, що це звучить дивно в ці дні рівномірно несегментованих несегментованих архітектур, але стандартне ОБОВ'ЯЗКОВО потребує широкого розмаїття, ніж "що нормально в 2009 році", ви знаєте! -)


6
Це, поряд з численними іншими , які стрибали до такого ж висновку, пояснює різницю між size_tі , uintptr_tкрім того, що про ptrdiff_tта intptr_t- з обидва з них буде мати можливість зберігати один і той же діапазон значень майже на будь-якій платформі? Чому цілі цілі підписані та непідписані вказівники розміру, особливо якщо вони ptrdiff_tвже призначені цілому цілому типу підписаного розміру.
Кріс Лутц

8
Ключова фраза там " майже на будь-якій платформі", @Chris. Реалізація вільна обмежувати покажчики діапазоном 0xf000-0xffff - для цього потрібен 16-бітовий intptr_t, але лише 12/13-бітний ptrdiff_t.
paxdiablo

29
@Chris, лише для покажчиків всередині одного масиву чітко визначено, щоб прийняти їх різницю. Так, на абсолютно однакових сегментованих 16-бітних архітектурах (масив повинен жити в одному сегменті, але два різних масиви можуть бути в різних сегментах) покажчики повинні бути 4 байти, але різниця покажчиків може бути 2 байти!
Алекс Мартеллі

6
@AlexMartelli: За винятком того, що різниці вказівників можуть бути позитивними чи негативними. Стандарт size_tповинен мати принаймні 16 біт, але ptrdiff_tбути принаймні 17 біт (що на практиці означає, що це, мабуть, буде принаймні 32 біта).
Кіт Томпсон

3
Не зважаючи на сегментовані архітектури, що робити з такою сучасною архітектурою, як x86-64? Рання реалізація цієї архітектури дає лише 48-бітний адресний простір, але самі вказівники - це 64-бітний тип даних. Найбільший суміжний блок пам'яті, на який ви могли б обґрунтуватись, був би 48-розрядним, тому я повинен уявити, що SIZE_MAXце не повинно бути 2 ** 64. Зверніть увагу на використання плоскої адреси; жодна сегментація не потрібна для того, щоб мати невідповідність між SIZE_MAXі діапазоном вказівника даних.
Андон М. Коулман

89

Щодо вашої заяви:

"Стандарт C гарантує, що size_tце тип, який може містити будь-який індекс масиву. Це означає, що, логічно, size_tмає бути можливість утримувати будь-який тип вказівника."

Це насправді помилковість (помилкове уявлення внаслідок неправильного міркування) (а) . Ви можете подумати, що останнє випливає з першого, але це насправді не так.

Покажчики та індекси масивів - це не одне і те ж. Цілком правдоподібно передбачити відповідну реалізацію, яка обмежує масиви до 65536 елементів, але дозволяє покажчикам адресувати будь-яке значення в масивний 128-бітний адресний простір.

C99 стверджує, що верхня межа size_tзмінної визначається SIZE_MAXі може бути такою низькою, як 65535 (див. C99 TR3, 7.18.3, без змін у C11). Покажчики були б досить обмеженими, якби вони були обмежені цим діапазоном у сучасних системах.

На практиці ви, ймовірно, виявите, що ваше припущення справедливо, але це не тому, що стандарт це гарантує. Тому що це насправді не гарантує.


(а) Це , до речі, не є якоюсь формою особистої атаки, а лише вказати, чому ваші твердження помилкові в контексті критичного мислення. Наприклад, такі міркування також недійсні:

Всі цуценята милі. Ця річ мила. Тому ця річ повинна бути цуценятою.

Симпатичність чи інакше цуценя тут не має жодного стосунку, я лише констатую, що два факти не призводять до висновку, адже перші два речення дозволяють існувати милі речі, які не є цуценятами.

Це схоже на ваше перше твердження, не обов'язково наказуючи друге.


Замість того, щоб повторити те, що я сказав у коментарях до Алекса Мартеллі, я просто скажу подяку за уточнення, але ще раз повторюю другу половину мого питання ( ptrdiff_tпроти intptr_tчастини).
Кріс Лутц

5
@Ivan, як і у більшості комунікацій, має бути спільне розуміння певних основних елементів. Якщо ви бачите цю відповідь як "забави", я запевняю, що це нерозуміння мого наміру. Якщо припустити, що ви посилаєтесь на мій коментар "логічної помилки" (я не бачу іншої можливості), це означало як фактичне твердження, а не якесь твердження, зроблене коштом ОП. Якщо ви хотіли б запропонувати деякі конкретні поліпшення , щоб звести до мінімуму можливість неправильного розуміння (а не тільки загальної скарги), я був би радий , щоб розглянути.
paxdiablo

1
@ivan_pozdeev - це неприємна і різка пара правок, і я не бачу жодних доказів того, що paxdiablo нікого не забавляв. Якби я був ОП, я відкотив би це право ....
ex nihilo

1
@Ivan, не дуже задоволений запропонованими вами змінами, відкотився, а також намагався усунути будь-яке ненавмисне правопорушення. Якщо у вас є якісь інші зміни, пропоную розпочати чат, щоб ми могли обговорити.
paxdiablo

1
@paxdiablo гаразд, я гадаю, "це насправді помилка" є менш прихильним.
ivan_pozdeev

36

Я дозволю, щоб усі інші відповіді стояли на собі щодо міркувань із обмеженнями сегментів, екзотичними архітектурами тощо.

Чи не достатньо простої різниці в назвах , щоб використовувати належний тип для належної речі?

Якщо ви зберігаєте розмір, використовуйте size_t. Якщо ви зберігаєте вказівник, використовуйте intptr_t. Людина, що читає ваш код, миттєво дізнається, що "ага, це розмір чогось, ймовірно, в байтах", і "о, ось значення вказівника з певних причин зберігається як ціле число".

В іншому випадку ви могли просто використовувати unsigned long(або, у ці тут сучасні часи unsigned long long), для всього. Розмір - це не все, назви типів мають значення, яке корисно, оскільки це допомагає описати програму.


Я згоден, але я розглядав щось злому / фокус (що я б чітко документував, звичайно), що стосується зберігання типу вказівника у size_tполі.
Кріс Лутц

@MarkAdler Standard не вимагає, щоб покажчики були представлені як цілі числа: Будь-який тип покажчика може бути перетворений на цілий тип. За винятком випадків, зазначених раніше, результат визначається реалізацією. Якщо результат не може бути представлений у цілому типі, поведінка не визначена. Результат не повинен знаходитися в діапазоні значень будь-якого цілого числа. Таким чином, тільки void*, intptr_tі uintptr_tгарантовано мати можливість представляти який - або покажчик на дані.
Андрій Світличний

12

Можливо, що розмір найбільшого масиву менший за вказівник. Подумайте про сегментовані архітектури - покажчики можуть бути 32-бітовими, але один сегмент може мати адресу лише 64 КБ (наприклад, стара архітектура 8086 реального режиму).

Хоча вони вже не часто використовуються в настільних машинах, стандарт C призначений для підтримки навіть невеликих спеціалізованих архітектур. Досі існують вбудовані системи, наприклад, з 8 або 16 бітними процесорами.


Але ви можете індексувати покажчики так само, як масиви, тож чи повинні size_tбути в змозі це впоратися? Або динамічні масиви в якомусь далекому сегменті все ще обмежуються індексуванням у межах їх сегмента?
Кріс Лутц

Покажчики індексації підтримуються лише технічно за розміром масиву, на який вони вказують, - тож якщо масив обмежений розміром 64 КБ, це все, що має підтримувати арифметика вказівника. Однак компілятори MS-DOS все ж підтримували «величезну» модель пам’яті, де далеко вказівники (32-бітні сегментовані покажчики) маніпулювали, щоб вони могли адресувати всю пам’ять як єдиний масив - але арифметика, зроблена для покажчиків за кадром, була досить потворно - коли зміщення збільшується минуле значення 16 (або щось таке), зміщення повертається до 0, а частина сегменту збільшується.
Майкл Берр

7
Читайте en.wikipedia.org/wiki/C_memory_model#Memory_segmentation і плачте за програвачів MS-DOS, які загинули, щоб ми могли бути вільними.
Justicle

Гірше було те, що функція stdlib не подбала про ВЕЛИЧЕЗНЕ ключове слово. 16bit МС-С для всіх strфункцій і Борланд навіть для memфункцій ( memset, memcpy, memmove). Це означало, що ви можете перезаписати частину пам'яті, коли зміщення переповнене, що було цікаво налагоджувати на нашій вбудованій платформі.
Патрік Шлютер

@Justicle: Сегментована архітектура 8086 недостатньо підтримується в C, але я не знаю жодної іншої архітектури, яка була б більш ефективною у випадках, коли адресного простору в 1 МБ недостатньо, але 64K не було б. Деякі сучасні JVM фактично використовують адресацію дуже схоже на x86 в реальному режимі, використовуючи зміщення 32-бітових посилань на об'єкти ліворуч на 3 біти для генерування базових адрес об'єктів у адресному просторі 32 ГБ.
supercat

5

Я б подумав (і це стосується всіх типів назв), що він краще передає ваші наміри в коді.

Наприклад, навіть якщо unsigned shortі wchar_tмають той же розмір на Windows (я думаю), використовуючи wchar_tзамість unsigned shortпоказує намір , що ви будете використовувати його для зберігання широкого характеру, а не просто якимось - то довільне числа.


Але тут є різниця - в моїй системі wchar_tнабагато більше, ніж unsigned shortвикористання одного для іншого було б помилковим і створювало б серйозне (і сучасне) питання переносимості, тоді як проблеми переносимості між собою size_tі, uintptr_tздається, лежать у далеких краях 1980-х років (щось випадкове колоти в темряві на дату, там)
Chris Lutz

Touché! Але знову ж таки, size_tі uintptr_tдосі маються на увазі вживання у своїх назвах.
dreamlax

Вони це роблять, і я хотів дізнатися, чи існує мотивація цього, крім простої ясності. І виявляється, що є.
Кріс Лутц

3

Дивлячись і вперед, і вперед, і нагадуючи, що різні архітектурні дивні форми були розкидані по пейзажу, я впевнений, що вони намагалися обернути всі існуючі системи, а також забезпечити всі можливі майбутні системи.

Тож впевнене, що, як все налагоджувалося, нам досі потрібно не так багато типів.

Але навіть у LP64, досить поширеній парадигмі, для інтерфейсу системного виклику нам знадобилися size_t та ssize_t. Можна уявити собі більш обмежену спадщину чи майбутню систему, коли використання повного 64-розрядного типу є дорогим, і вони можуть захотіти заїхати на операційні входи / виводи більше 4 Гб, але все ще мають 64-бітні покажчики.

Я думаю, що вам доведеться задуматися: що може бути розроблено, що може прийти в майбутньому. (Можливо, 128-розрядні вказівники з розподіленою системою в Інтернеті, але не більше 64 біт в системному виклику, або, можливо, навіть "спадковий" 32-бітний ліміт. .

Також подивіться, що тоді існувало. Окрім моделей пам'яті в реальному режимі на мільйон 286, як щодо 60-бітових мейнфреймів слів / 18-бітових вказівників CDC? Як щодо серії Cray? Не майте на увазі нормальних ILP64, LP64, LLP64. (Я завжди вважав, що мікрософт претендує на LLP64, це повинен був бути P64.) Я, безумовно, можу уявити, що комітет намагається охопити всі бази ...


-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;
}

Маючи на увазі, що intptr_t завжди повинен замінювати size_t та візу.


10
Все це показує особливу синтаксичну вигадку C. Індексація масиву визначається в термінах, коли x [y] еквівалентно * (x + y), і оскільки a + 3 і 3 + a є однаковими за типом і значенням, ви можете використовувати 3 [a] або a [3].
Фред Нурк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.