Почему v в C означает не void?

25

В строго типизированных языках, таких как Java и C #, void(или Void) тип возвращаемого значения для метода, по-видимому, означает:

Этот метод ничего не возвращает. Ничего такого. Без возврата. Вы не получите ничего от этого метода.

Что действительно странно, так это то, что в C voidкак тип возвращаемого значения или даже как тип параметра метода означает:

Это может быть что угодно. Вы должны прочитать исходный код, чтобы узнать. Удачи. Если это указатель, вы должны действительно знать, что вы делаете.

Рассмотрим следующие примеры в C:

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в одном случае для устранения неоднозначности.
Мычанка
4
@ Мефи, они не совсем строгие? Под C # с использованием утиной типизации вы подразумеваете dynamicтип, который используется редко?
Axarydax
9
@Mephy: В прошлый раз, когда я проверял Википедию, перечислил одиннадцать взаимно противоречивых значений для "строго типизированных". Сказать, что «C # не является строго типизированным», поэтому бессмысленно.
Эрик Липперт
8
@LightnessRacesinOrbit: Нет, дело в том, что нет никакого авторитетного определения термина. Википедия не является предписывающим ресурсом, который говорит вам, каково правильное значение термина. Это описательный ресурс. Тот факт, что он описывает одиннадцать различных противоречивых значений, свидетельствует о том, что этот термин нельзя использовать в разговоре без тщательного его определения . Если вы поступите иначе, значит, шансы очень хорошие, люди в разговоре будут говорить друг с другом в прошлом.
Эрик Липперт

Ответы:

58

Ключевое слово void(не указатель) означает «ничего» на этих языках. Это соответствует.

Как вы заметили, void*означает «указатель на что-либо» в языках, которые поддерживают необработанные указатели (C и C ++). Это неудачное решение, потому что, как вы упомянули, оно voidозначает две разные вещи.

Я не смог найти историческую причину повторного использования voidдля обозначения «ничего» и «что-нибудь» в разных контекстах, однако С делает это в нескольких других местах. Например, staticимеет разные цели в разных контекстах. Очевидно, что в языке Си есть прецедент повторного использования ключевых слов, независимо от того, что вы думаете о практике.

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*.
Джеймсдлин
20
Даже void *, voidозначает «ничего», а не «ничего». Вы не можете разыменовать a void *, вам нужно сначала привести его к другому типу указателя.
oefe
2
@oefe Я думаю , что это не имеет смысла , чтобы разделить , что волосы, так voidи void*различные типы ( на самом деле, void«нет тип»). Это делает столько же смысла , как говорить « intв int*то значит.» Нет, когда вы объявляете переменную-указатель, вы говорите: «Это адрес памяти, который может содержать что-то». В случае void*, если вы говорите «этот адрес содержит что-то, но что-то не может быть выведено из типа указателя».
32

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

Все указатели в C должны иметь возможность разыменования. Если вы разыменовываете a void*, какой тип вы ожидаете получить? Помните, что указатели C не содержат никакой информации о типе среды выполнения, поэтому тип должен быть известен во время компиляции.

Учитывая этот контекст, единственное, что вы можете логически сделать с разыменованным, void*- это игнорировать его, что в точности соответствует voidтипу поведения .

Карл Билефельдт
источник
1
Я согласен, пока вы не доберетесь до части «игнорируй это». Возможно, что возвращенная вещь содержит информацию о том, как ее интерпретировать. Другими словами, это может быть C-эквивалент BASIC-варианта. «Когда вы получите это, взгляните, чтобы понять, что это такое - я не могу сразу сказать вам, что вы найдете».
Флорис
1
Или, иначе говоря, гипотетически я мог бы поместить определяемый программистом «дескриптор типа» в первый байт в ячейку памяти, адресуемую указателем, что указывало бы мне, какой тип данных я могу ожидать найти в следующих байтах.
Роберт Харви
1
Конечно, но в Си было решено оставить эти детали на усмотрение программиста, а не запекать их на язык. С языковой точки зрения, он не знает, что еще делать, кроме как игнорировать. Это позволяет избежать издержек, связанных с постоянным включением информации о типе в качестве первого байта, когда в большинстве случаев вы можете определить ее статически.
Карл Билефельдт
4
@RobertHarvey да, но тогда вы не разыменовываете указатель на пустоту; вы приводите пустой указатель к указателю на символ, а затем разыменовываете указатель на символ.
user253751
4
@ Флорис gccне согласна с тобой. Если вы попытаетесь использовать это значение, gccвыдайте сообщение об ошибке error: void value not ignored as it ought to be.
Касперд
19

Возможно, было бы более полезно думать о voidкачестве возвращаемого типа. Тогда ваш второй метод будет читать «метод, который возвращает нетипизированный указатель».

Роберт Харви
источник
Что помогает. Как человек, который (наконец-то!) Изучает C после нескольких лет, проведенных на объектно-ориентированных языках, указатели - это нечто совершенно новое для меня. Кажется, что при возврате указателя voidон примерно эквивалентен *языкам, подобным ActionScript 3, что просто означает «любой тип возврата».
Нафтули Кей
5
Ну, voidэто не подстановочный знак. Указатель - это просто адрес памяти. Помещение типа перед *надписью «вот тип данных, который вы можете ожидать найти по адресу памяти, указанному этим указателем». Помещение voidперед *компилятором говорит: «Я не знаю тип; просто верните мне необработанный указатель, и я выясню, что делать с данными, на которые он указывает».
Роберт Харви
2
Я бы даже сказал, что void*указывает на «пустоту». Пустой указатель, на который он не указывает. Тем не менее, вы можете разыграть его как любой другой указатель.
Роберт
11

Давайте немного ужесточим терминологию.

Из онлайн-стандарта C 2011 :

6.2.5 Типы
...
19 voidТип содержит пустой набор значений; это неполный тип объекта, который не может быть завершен.
...
6.3 Преобразования
...
6.3.2.2 void

1 (несуществующее) значение voidвыражения (выражение, имеющее тип void) ни в коем случае не должно использоваться, и неявные или явные преобразования (кроме void) не должны применяться к такое выражение. Если выражение любого другого типа оценивается как void выражение, его значение или обозначение отбрасывается. ( voidВыражение оценивается на предмет его побочных эффектов.)

6.3.2.3 Указатели

1 Указатель наvoidможет быть преобразован в или из указателя на любой тип объекта. Указатель на любой тип объекта может быть преобразован в указатель на void и обратно; результат должен сравниваться равным исходному указателю.

voidВыражение не имеет никакого значения (хотя она может иметь побочные эффекты). Если у меня есть функция, определенная для возврата void, вот так:

void foo( void ) { ... }

тогда звонок

foo();

не оценивает значение; Я не могу присвоить результат чему-либо, потому что нет результата.

Указатель на voidэто по существу «общий» тип указателя; Вы можете присвоить значение void *любому другому типу указателя объекта без необходимости явного приведения (именно поэтому все программисты на C будут кричать на вас для приведения результата malloc).

Вы не можете напрямую разыменовывать a void *; Вы должны сначала назначить его другому типу указателя объекта, прежде чем сможете получить доступ к указанному объекту.

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

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

Джон Боде
источник
9

В Java и C # void в качестве возвращаемого типа для метода, по-видимому, означает: этот метод ничего не возвращает. Ничего такого. Без возврата. Вы не получите ничего от этого метода.

Это утверждение верно. Это верно и для C и C ++.

в C void как тип возвращаемого значения или даже как тип параметра метода означает нечто иное.

Это утверждение неверно. voidвозвращаемый тип в C или C ++ означает то же самое, что и в C # и Java. Вы путаете voidс 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 void_array[42];- это тоже нонсенс.

Указатель на void ( void*) - это нечто иное. Это указатель, а не массив. Читайте void*как означающий указатель, который указывает на что-то "без значения". Указатель «без значения», что означает, что нет смысла разыменовывать такой указатель. Попытка сделать это фактически незаконна. Код, разыменовывающий пустой указатель, не будет компилироваться.

Так что, если вы не можете разыменовать пустой указатель, и вы не можете создать массив пустот, как вы можете их использовать? Ответ в том, что указатель на любой тип может быть приведен к и от void*. Приведение некоторого указателя на void*и возвращение указателя на исходный тип приведет к значению, равному исходному указателю. Стандарт C гарантирует такое поведение. Основная причина, по которой вы видите так много пустых указателей в C, заключается в том, что эта возможность обеспечивает способ реализации скрытия информации и объектно-ориентированного программирования на языке, который в значительной степени не является объектно-ориентированным языком.

Наконец, причина, по которой вы часто видите moveобъявленный void *move(...)скорее, чем тот void* move(...), что в том, что размещение звездочки рядом с именем, а не с типом, является очень распространенной практикой в ​​C. Причина в том, что следующее объявление доставит вам большие неприятности: int* iptr, jptr;Это сделает вас Я думаю, что вы объявляете два указателя на int. Ты не. Это заявление делает , а не указатель на . Правильное объявление: Вы можете делать вид, что звездочка принадлежит типу, если придерживаетесь практики объявления только одной переменной на оператор объявления. Но имейте в виду, что вы притворяетесь.jptrintintint *iptr, *jptr;

Дэвид Хаммен
источник
«Код, разыменовывающий нулевой указатель, не будет компилироваться». Я думаю, вы имеете в виду код, который разыменовывает указатель void, который не будет компилироваться. Компилятор не защищает вас от разыменования нулевого указателя; это проблема времени выполнения.
Коди Грей
@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 все является объектом, и это позволяет среде выполнения отслеживать каждый объект. Это не обязательно должно знать, что мой объект - Автомобиль, ему просто нужно знать некоторую базовую информацию, которая уже позаботилась о классе Object.

Для нас, программистов, это означает, что большая часть информации, которую мы должны были бы обрабатывать вручную в C / C ++, заботится о нас для класса Object, устраняя необходимость в специальном случае, который является указателем.

Thebluefish
источник
«моя пустота * указывает на что-то без определенного типа» - не совсем так. Правильнее будет сказать, что это указатель на значение нулевой длины (почти как массив с 0 элементами, но даже без информации о типе, связанной с массивом).
Скотт Уитлок
2

Я попытаюсь сказать, как можно думать об этих вещах на C. Официальный язык в стандарте C не совсем удобен void, и я не буду пытаться полностью соответствовать этому.

Тип voidне является первоклассным гражданином среди типов. Хотя это тип объекта, он не может быть типом любого объекта (или значения), поля или параметра функции; однако это может быть тип возвращаемого значения функции и, следовательно, тип выражения (в основном это вызовы таких функций, но выражения, сформированные с помощью условного оператора, также могут иметь тип void). Но даже минимальное использование voidв качестве типа значения, для которого вышеизложенное оставляет дверь открытой, а именно завершение функции, возвращаемой voidс помощью оператора формы, return E;где Eесть выражение типа void, явно запрещено в C (хотя это разрешено в 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 на этих языках, voidздесь не обязательно означает «что-то или ничего».

Reg Edit
источник
0

В C возможно иметь указатель на вещи любого типа [указатели ведут себя как тип ссылки]. Указатель может идентифицировать объект известного типа или он может идентифицировать объект произвольного (неизвестного) типа. Создатели C решили использовать тот же общий синтаксис для «указателя на предмет произвольного типа» и для «указателя на предмет определенного типа». Поскольку синтаксис в последнем случае требует, чтобы токен указывал, что это за тип, первый синтаксис требует размещения чего-то, где этот токен принадлежал бы, если бы тип был известен. C решил использовать ключевое слово «void» для этой цели.

В Паскале, как и в С, можно иметь указатели на вещи любого типа; более популярные диалекты языка Паскаль также допускают «указатель на вещь произвольного типа», но вместо того, чтобы использовать тот же синтаксис, который они используют для «указателя на вещь определенного типа», они просто ссылаются на первый как Pointer.

В Java нет пользовательских типов переменных; все это либо примитив, либо ссылка на объект кучи. Можно определить вещи типа «ссылка на объект кучи определенного типа» или «ссылка на объект кучи произвольного типа». Первый просто использует имя типа без какой-либо специальной пунктуации, чтобы указать, что это ссылка (поскольку это не может быть что-либо еще); последний использует имя типа Object.

Использование в void*качестве номенклатуры для указателя на что-то произвольного типа, насколько я знаю, уникально для C и прямых производных, таких как C ++; другие языки, которые я знаю, обрабатывают эту концепцию, просто используя четко названный тип.

Supercat
источник
-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*является указателем в памяти на встроенный тип с нулевой длиной. Тот факт, что вы, возможно, знаете, что сразу после этого есть что-то, что может быть полезно для вас, - это другой вопрос, требующий от вас действительно знать, что вы делаете (и это опасно).

Скотт Уитлок
источник
Единственный известный мне компилятор, который назначает длину типу void, это gcc, а gcc назначает ему длину 1.
Джошуа