Чому пустота в C означає не порожнечу?


25

У сильно типізованих мовах, таких як Java та C #, void(або Void) як тип повернення для методу, схоже, означає:

Цей метод нічого не повертає. Нічого. Ніякого повернення. Ви нічого не отримаєте від цього методу.

Що насправді дивно, це те, що в C, voidяк тип повернення або навіть як тип параметру методу, означає:

Це справді може бути будь-що. Щоб дізнатись, вам доведеться прочитати вихідний код. Удачі. Якщо це вказівник, ви дійсно повинні знати, що ви робите.

Розглянемо наступні приклади на С:

void describe(void *thing)
{
    Object *obj = thing;
    printf("%s.\n", obj->description);
}

void *move(void *location, Direction direction)
{
    void *next = NULL;

    // logic!

    return next;
}

Очевидно, другий метод повертає вказівник, який за визначенням міг би бути будь-яким.

Оскільки C старіший за Java та C #, чому ці мови прийняли voidзначення "нічого", тоді як C використовував це як "нічого або що-небудь (коли вказівник)"?


138
Заголовок вашого запитання та текст розповідає про те, voidколи використовується приклад коду, void*який є зовсім іншим.

4
Також зауважте, що Java і C # мають одне і те ж поняття, вони просто називають його Objectв одному випадку, щоб не розігрувати.
Mooing Duck

4
@Mephy вони не дуже сильно набрані? Під C #, використовуючи dynamicтипи качок, ви маєте на увазі тип, який рідко використовується?
Аксарідакс

9
@Mephy: Востаннє я перевіряв, що Вікіпедія перераховувала одинадцять взаємно суперечливих значень для "сильно набраних". Сказати, що "C # не сильно набраний", тому безглуздо.
Ерік Ліпперт

8
@LightnessRacesinOrbit: Ні, справа в тому, що не існує авторитетного визначення терміна. Вікіпедія - це не приписуючий ресурс, який говорить вам, що таке правильне значення терміна. Це описовий ресурс. Той факт, що він описує одинадцять різних суперечливих значень, є свідченням того, що цей термін не можна використовувати в розмові, не ретельно визначивши його . Інакше означає, що шанси дуже хороші, люди в розмові будуть говорити минуле, а не один для одного.
Ерік Ліпперт

Відповіді:


58

Ключове слово void(не вказівник) означає "нічого" в цих мовах. Це послідовно.

Як ви зазначали, void*означає "вказівник на що-небудь" в мовах, які підтримують необроблені покажчики (C і C ++). Це невдале рішення, оскільки, як ви вже згадали, воно voidозначає дві різні речі.

Мені не вдалося знайти історичну причину того, voidщо в різних контекстах відмовився означати "нічого" і "що-небудь", однак C робить це в кількох інших місцях. Наприклад, staticмає різні цілі в різних контекстах. Очевидно, що в мові C є прецедент для повторного використання ключових слів, незалежно від того, що можна думати про практику.

Java та C # досить різні, щоб зробити чисту перерву для виправлення деяких із цих проблем. Java і «безпечний» C # також не дозволяють сирі покажчики і не потрібно легко сумісність C (небезпечний C # робить дозволяють покажчики , але переважна більшість C # код не потрапляє в цю категорію). Це дозволяє їм трохи змінити речі, не турбуючись про зворотну сумісність. Одним із способів цього є введення класу Objectв корені ієрархії, від якої успадковуються всі класи, тому Objectпосилання виконує ту саму функцію void*без ганебності питань типу та необмеженого управління пам’яттю.


2
Я надам довідку, коли

4
They also do not allow raw pointers and do not need easy C compatibility.Помилковий. C # має покажчики. Вони зазвичай (і зазвичай повинні бути) відключені, але вони там.
Маг

8
Щодо того, чому вони повторно використовувались void: очевидною причиною було б уникати введення нового ключового слова (та потенційного порушення існуючих програм). Якби вони ввели спеціальне ключове слово (наприклад unknown), то це void*було б безглуздою (і, можливо, незаконною) конструкцією, і unknownбуло б законним лише у вигляді unknown*.
jamesdlin

20
Навіть у void *, voidозначає "нічого", а не "нічого". Ви не можете перенаправити a void *, вам потрібно передати його спочатку іншому типу вказівника.
oefe

2
@oefe Я думаю , що це не має сенсу , щоб розділити , що волосся, так voidі void*різні типи ( на насправді, void«немає тип»). Це робить стільки ж сенсу , як говорити « intв int*щось означає.» Ні, коли ви оголошуєте змінну вказівника, ви говорите "це адреса пам'яті, здатна щось утримувати". У випадку void*, коли ви говорите, "ця адреса містить щось, але щось не можна зробити з типу вказівника".

32

voidі void*це дві різні речі. voidв C означає точно те саме, що і в Java, відсутність повернутого значення. A void*- покажчик з відсутністю типу.

Усі покажчики на С повинні бути здатні бути відмежованими. Якщо ви дереферендували a void*, який тип ви б очікували отримати? Пам'ятайте, що покажчики C не несуть ніякої інформації про тип виконання, тому тип повинен бути відомий під час компіляції.

Зважаючи на цей контекст, єдине, що ви можете логічно зробити з дереференцією void*- це проігнорувати його, саме та поведінка, яку voidпозначає тип.


1
Я погоджуюсь, поки ви не досягнете біту "ігноруйте це". Можливо, що повернута річ містить інформацію про те, як її інтерпретувати. Іншими словами, це може бути еквівалентом С базового варіанту. "Коли ви це отримаєте, погляньте, щоб зрозуміти, що це таке - я не можу вам сказати наперед, що ви знайдете".
Флоріс

1
Або кажучи про інший спосіб, гіпотетично я міг би поставити визначений програмістом "тип дескриптора" в перший байт у місці пам'яті, адресованому вказівником, який би сказав мені, який тип даних я можу очікувати, щоб знайти у наступних байтах.
Роберт Харві

1
Звичайно, але в С було вирішено залишити ці деталі програмісту, а не викласти їх на мову. З мовної точки зору, вона не знає, що ще робити, крім ігнорування. Це дозволяє уникнути накладних витрат завжди включати інформацію типу як перший байт, коли в більшості випадків використання ви можете визначити її статично.
Карл Білефельдт

4
@RobertHarvey так, але тоді ви не скасовуєте недійсний покажчик; ви кидаєте недійсний покажчик на покажчик char, а потім перенаправляєте покажчик char.
користувач253751

4
@Floris gccне згоден з вами. Якщо ви спробуєте використати значення, gccстворіть повідомлення про помилку error: void value not ignored as it ought to be.
kasperd

19

Можливо, було б корисніше voidрозглянути як тип повернення. Тоді ваш другий метод прочитав би "метод, який повертає нетипізований покажчик".


Це допомагає. Як хтось, хто (нарешті!) Вивчає С після років, проведених на об'єктно-орієнтованих мовах, покажчики - це щось нове для мене поняття. Здається, що при поверненні вказівника voidприблизно еквівалентний таким *мовам, як ActionScript 3, що просто означає "будь-який тип повернення".
Naftuli Kay

5
Ну, voidце не майна. Вказівник - це лише адреса пам'яті. Поставивши тип перед *написом "ось тип даних, який ви можете очікувати на адресу пам'яті, на яку посилається цей вказівник". Поставляючи voidперед *сказати компілятору, "я не знаю тип; просто поверніть мені необроблений покажчик, і я зрозумію, що робити з даними, на які він вказує".
Роберт Харві

2
Я б навіть сказав, що void*бали на "порожнечу". Голий вказівник, на який він не вказує. Тим не менш, ви можете подати його, як і будь-який інший вказівник.
Роберт

11

Давайте трохи посилимо термінологію.

З онлайн-стандарту C 2011 :

6.2.5 Типи
...
19 voidТип містить порожній набір значень; це неповний тип об'єкта, який неможливо виконати.
...
6.3 Конверсії
...
6.3.2.2 недійсні

1 (Неіснуюче) значення voidвиразу (вираз, що має тип void) ні в якому разі не слід використовувати, а неявні або явні перетворення (крім void) не повинні застосовуватися до такий вираз. Якщо вираз будь-якого іншого типу оцінюється як void вираз, його значення або позначення відкидаються. ( voidВираз оцінюється за його побічними ефектами.)

6.3.2.3 Покажчики

1 Вказівник наvoidможе бути перетворений на або вказівник на будь-який тип об'єкта. Вказівник на будь-який тип об'єкта може бути перетворений у вказівник, щоб знову втратити чи повернути назад; результат порівнюється рівним вихідному покажчику.

А voidВираз не має ніякого значення (хоча вона може мати побічні ефекти). Якщо у мене визначена функція повернення void, наприклад:

void foo( void ) { ... }

то дзвінок

foo();

не оцінює значення; Я не можу призначити результат ні в чому, тому що результату немає.

покажчик на voidце по суті «загальний» тип покажчика; ви можете призначити значення void *будь-якому іншому типу вказівника, не потребуючи чіткого виступу (саме тому всі програмісти C будуть кричати на вас за те, що ви кинули результат malloc).

Ви не можете безпосередньо знешкодити a void *; спочатку потрібно призначити його іншому типу вказівника об'єкта, перш ніж ви зможете отримати доступ до об'єкта із загостреним об'єктом.

voidпокажчики використовуються для реалізації (більш-менш) загальних інтерфейсів; канонічним прикладом єqsort функція бібліотеки, яка може сортувати масиви будь-якого типу, якщо ви надаєте функцію порівняння, усвідомлювану тип.

Так, використання одного і того ж ключового слова для двох різних понять (без значення та загального вказівника) є заплутаним, але це не так, як немає прецеденту; staticмає кілька чітких значень як у C, так і в C ++.


9

У Java та C # недійсність як тип повернення методу, схоже, означає: цей метод нічого не повертає. Нічого. Ніякого повернення. Ви нічого не отримаєте від цього методу.

Це твердження правильне. Це правильно і для C, і для C ++.

в C, void як тип повернення або навіть як тип параметра параметра означає щось інше.

Це твердження невірне. voidяк тип повернення в C або C ++ означає те саме, що і в C # і Java. Ви плутаєте voidз void*. Вони абсолютно різні.

Хіба це не бентежить voidі void*означає дві абсолютно різні речі?

Так.

Що таке покажчик? Що таке порожній покажчик?

Покажчик є значенням , яке може бути разименовиваются . Перенаправлення дійсного вказівника дає місце зберігання типу загостреного типу . Недійсний покажчик є покажчиком , який не має будь якого особливого вказується тип; він повинен бути перетворений у більш конкретний тип вказівника перед тим, як значення буде відмінено для створення місця зберігання .

Чи однакові недійсні покажчики у C, C ++, Java та C #?

У Java немає недійсних покажчиків; C # робить. Вони такі самі, як у C та C ++ - значення вказівника, яке не має жодного конкретного типу, пов’язаного з ним, яке повинно бути перетворене на більш конкретний тип перед тим, як відкинути його для створення місця зберігання.

Оскільки C старший за Java та C #, чому ці мови прийняли void як значення "нічого", тоді як C використовував це як "нічого або нічого (коли вказівник)"?

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

Чому Java та C # прийняли конвенцію, яка voidє дійсним типом повернення, а не, наприклад, використовуючи конвенцію Visual Basic, що функції ніколи не повертаються до недійсних, а підпрограми завжди повертаються до недійсності?

Щоб бути знайомим програмістам, які приходять на Java або C # з мов, де voidє тип повернення.

Чому C # прийняв заплутану конвенцію, яка void*має зовсім інше значення, ніж void?

Щоб бути знайомим програмістам, які відвідують C # з мов, де void*є тип вказівника.


5
void describe(void *thing);
void *move(void *location, Direction direction);

Перша функція нічого не повертає. Друга функція повертає недійсний покажчик. Я би оголосив цю другу функцію як

void* move(void* location, Direction direction);

Якщо може допомогти, якщо ви вважаєте, що "недійсне" означає "без сенсу". Повернене значення describe"без сенсу". Повернення значення з такої функції є незаконним, оскільки ви сказали компілятору, що повернене значення не має сенсу. Ви не можете зафіксувати повернене значення з цієї функції, оскільки це без сенсу. Ви не можете оголосити змінну типу, voidоскільки вона не має сенсу. Наприклад, void nonsense;це нісенітниця і насправді є незаконною. Також зауважте, що масив недійснихvoid void_array[42]; також є нісенітницею.

Вказівник на void ( void*) - це щось інше. Це вказівник, а не масив. Прочитайтеvoid* як означає вказівник, який вказує на щось "без сенсу". Вказівник "не має сенсу", а це означає, що не має сенсу знецінювати такий покажчик. Намагання зробити це насправді незаконно. Код, що відмежування, недійсний покажчик не буде скомпільований.

Отже, якщо ви не можете розмежувати покажчик недійсності і не можете створити масив порожнеч, як ви можете їх використовувати? Відповідь полягає в тому, що вказівник на будь-який тип може бути переданий на та з нього void*. Закидання деякого вказівника на void*та повернення до вказівника до оригінального типу дасть значення, рівне вихідному покажчику. Стандарт С гарантує таку поведінку. Основна причина, по якій ви бачите так багато недійсних покажчиків на C, полягає в тому, що ця можливість забезпечує спосіб реалізації приховування інформації та програмування на основі об'єктів на тому, що дуже не є об'єктно орієнтованою мовою.

Нарешті, причина, яку ви часто бачите moveяк оголошену, void *move(...)а не те void* move(...), що ставити зірочку поруч із іменем, а не типом, є досить поширеною практикою у C. Причиною є те, що наступна декларація спричинить вам великі проблеми: int* iptr, jptr;Це призведе до вас думаю, ти оголошуєш два покажчики на "an" int. Ти не. Ця заява робить , а не покажчик на . Правильна заява: Ви можете зробити вигляд, що зірочка належить до типу, якщо ви дотримуєтесь практики оголошення лише однієї змінної на кожне твердження. Але майте на увазі, що ви робите вигляд.jptrintintint *iptr, *jptr;


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

@CodyGray - Дякую! Виправлено це. Код, який перенаправляє нульовий покажчик, буде компілювати (доки покажчик не буде void* null_ptr = 0;).
Девід Хаммен

2

Простіше кажучи, різниці немає . Дозвольте навести кілька прикладів.

Скажіть, у мене клас під назвою "Автомобіль".

Car obj = anotherCar; // obj is now of type Car
Car* obj = new Car(); // obj is now a pointer of type Car
void obj = 0; // obj has no type
void* = new Car(); // obj is a pointer with no type

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

Це пов’язано з тим, що вказівник - особливий випадок. Об'єкт вказівника завжди вказуватиме на місце в пам’яті, чи є там дійсний об’єкт чи ні. Будь мої void* carпосилання на байт, слово, автомобіль чи велосипед не мають значення, оскільки мої вказівки void*на щось без визначеного типу. Нам вже згодом визначити це.

У таких мовах, як C # і Java, керується пам'яттю, а поняття покажчиків перекочується в клас Object. Замість того, щоб мати посилання на об’єкт типу "нічого", ми знаємо, що принаймні наш об'єкт має тип "Об'єкт". Дозвольте мені пояснити, чому.

У звичайному C / C ++, якщо ви створюєте об'єкт і видаляєте його вказівник, отримати доступ до цього об’єкта неможливо. Це те, що відомо як витік пам'яті .

У C # / Java все є об'єктом, і це дозволяє виконувати час виконання кожного об'єкта. Не обов'язково знати, що мій об’єкт - це Автомобіль, він просто повинен знати деяку основну інформацію, про яку вже піклується клас Об'єкт.

Для нас, програмістів, це означає, що велика частина інформації, про яку нам доведеться вручну опікуватися в C / C ++, опікується нами класом Object, усуваючи потребу в спеціальному випадку, який є вказівником.


"моя пустота * вказує на щось без визначеного типу" - не зовсім. Правильніше сказати, що це вказівник на значення нульової довжини (майже як масив з 0 елементами, але навіть без інформації про тип, пов’язаної з масивом).
Скотт Вітлок

2

Я спробую сказати, як можна було б думати про ці речі в C. Офіційній мові стандарту С насправді не легко void, і я не намагатимусь цілком відповідати цьому.

Тип voidне є першокласним громадянином серед типів. Хоча це тип об'єкта, він не може бути типом будь-якого об'єкта (або значення), поля або параметра функції; однак це може бути типом повернення функції і, отже, типом виразу (в основному викликів таких функцій, але вирази, утворені за допомогою умовного оператора, також можуть мати тип void). Але навіть мінімальне використання voidяк типу значення, яке вище залишає двері відкритими, а саме закінчення функції, що повертається voidвикладом форми, return E;де Eє виразом типу void, явно заборонено в C (це дозволено в C ++, хоча, але з причин, що не стосуються С).

Якби об'єкти типу voidбули дозволені, вони мали б 0 біт (тобто кількість інформації у значенні виразу voidтипу); це не буде великою проблемою з дозволом таких об'єктів, але вони будуть досить марними (об'єкти розміром 0 створюють певні труднощі з визначенням арифметики вказівника, що, можливо, краще просто буде заборонено). Набір різних значень такого об’єкта мав би один (тривіальний) елемент; його значення можна сприймати, тривіально, тому що в ньому немає жодної речовини, але воно не може бути змінене (не маючи жодного іншого значення). Тому стандарт заплутаний у висловлюванні voidмістить порожній набір значень; такий тип може бути корисним для опису виразів, які не можутьпідлягають оцінці (наприклад, стрибки або невикличні дзвінки), хоча мова С фактично не використовує такого типу.

Будучи забороненим як тип значення, voidбуло використано щонайменше за двома безпосередньо пов'язаними способами позначення "заборонених": вказати (void)як специфікацію параметрів у типах функцій означає, що надання будь-яких аргументів заборонено (тоді як "()" буде означати навпаки все і так, навіть надання voidвираження як аргументу функціям із (void)специфікацією параметра заборонено), а декларування void *pозначає, що вираз перенаправлення *pзаборонений (але не те, що це дійсний вираз типу void). Отже, ви праві, що цим способам використання voidнасправді не відповідаютьvoid дійсному типу виразів. Однак об'єкт типуvoid*насправді не є вказівником на значення будь-якого типу, воно означає значення, яке трактується як покажчик, хоча воно не може бути відмежоване, а арифметика вказівника з ним заборонена. Тоді "трактується як вказівник" тоді насправді лише означає, що він може бути переданий з будь-якого типу вказівника без втрати інформації. Однак він також може бути використаний для передачі цілих значень до та з, тому не потрібно взагалі нічого вказувати.


Строго кажучи, він може бути переданий з будь-якого типу вказівника об'єкта без втрати інформації, але не завжди на та з нього . Як правило, void*перехід від іншого типу вказівника може втратити інформацію (або навіть мати UB, я не можу згадати про себе), але якщо значення, void*отримане від лиття вказівника того ж типу, на який ви зараз викидаєте, то це не так.
Стів Джессоп

0

У C # та Java кожен клас походить від Objectкласу. Отже, коли ми хочемо передати посилання на "щось", ми можемо використовувати посилання типу Object.

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


0

У C можна вказати на речі будь-якого типу [покажчики ведуть себе як тип посилання]. Вказівник може ідентифікувати об'єкт відомого типу, або він може ідентифікувати об'єкт довільного (невідомого) типу. Творці C вирішили використовувати той самий загальний синтаксис для "вказівника на річ довільного типу", що і для "вказівника на річ певного типу". Оскільки синтаксис в останньому випадку вимагає маркер, щоб вказати, що таке тип, колишній синтаксис вимагає поставити щось, де цей маркер належав би, якщо тип був відомий. C вирішив використовувати для цього ключове слово "void".

У Паскалі, як і в С, можна вказати на речі будь-якого типу; Більш популярні діалекти Паскаля також дозволяють "вказувати на річ довільного типу", але замість того, щоб використовувати той самий синтаксис, який вони використовують для "вказівника на річ певного типу", вони просто посилаються на колишнє як Pointer.

У Java немає визначених користувачем типів змінних; все є або примітивним, або посиланням на купу об'єктів. Можна визначити речі типу "посилання на об'єкт купи певного типу" або "посилання на об'єкт купи довільного типу". Перший просто використовує ім'я типу без спеціальних розділових знаків, щоб вказати, що це посилання (оскільки воно не може бути нічого іншого); останній використовує назву типу Object.

Використання void*в якості номенклатури вказівника на щось довільного типу настільки, наскільки я знаю, унікальне для C та прямих похідних, таких як C ++; інших мов, які я знаю, поводжусь із цією концепцією, просто використовуючи чітко названий тип.


-1

Ви можете думати про таке ключове слово, voidяк порожнє struct( у C99 порожні структури, мабуть, заборонені , у .NET та C ++ вони займають більше 0 пам'яті, і останнє, я пригадую, все в Java - це клас):

struct void {
};

... це означає, що це тип з 0 довжиною. Ось як це працює, коли ви робите виклик функції, який повертає void... замість виділення 4-х байт або так на стеку для цілого значення повернення, він взагалі не змінює покажчик стека (збільшуючи його на довжину 0 ).

Отже, якщо ви оголосили деякі змінні типу:

int a;
void b;
int c;

... і тоді ви взяли адресу цих змінних, то, можливо, bі cможуть бути розташовані там же в пам'яті, оскільки bмає нульову довжину. Отже, a void*- це вказівник в пам'яті на вбудований тип з нульовою довжиною. Те, що ви можете знати, що відразу після цього є щось, що може бути для вас корисним, - інша справа, і вимагає, щоб ви дійсно знали, що ви робите (і це небезпечно).


Єдиний мені компілятор, який мені призначає довжину для введення пустоти, - це gcc, а gcc призначає їй довжину 1.
Джошуа
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.