Может ли компилятор привести `(void *) 0` в` execl (prog, arg, (void *) 0) `к нулевому указателю соответствующего типа? - PullRequest
0 голосов
/ 06 сентября 2018

из интерфейса программирования Linux

execl(prog, arg, (char *) 0);
execl(prog, arg, (char *) NULL);

Кастинг NULL в порядке последнего вызова выше обычно требуется , даже в тех реализациях, где NULL определяется как (void *) 0.

Это потому, что, хотя стандарты C требуют, чтобы нулевые указатели разных типов должны проверять истинность для сравнения на равенство, они не требуют, чтобы указатели разных типов имели одинаковый внутренний представление (хотя в большинстве реализаций они делают).
И, в функции с переменным числом, компилятор не может привести (void *) 0 на нулевой указатель соответствующего типа .

Стандарты C делают одно исключение из правила, что указатели разных типов не обязательно должны иметь одинаковое представление: указатели типы char * и void * должны иметь одинаковые внутреннее представительство . Это означает, что прохождение (void *) 0 вместо (char *) 0 не будет проблем в примере execl(), но в общем случае требуется приведение.

  1. "Как правило, требуется приведение NULL в порядке последнего вызова, указанного выше"

    Требует ли стандарт C, чтобы нулевой указатель был представлен так же, как (char*) 0?

  2. "в переменной функции, такой как execl(), компилятор не может привести (void *) 0 к нулевому указателю соответствующего типа."

    Не является ли (void *) 0 нулевым указателем типа?

    Если да, почему компилятор не может привести (void *) 0 в execl(prog, arg, (void*) 0) к «нулевому указателю соответствующего типа»?

  3. "указатели типов char * и void * должны иметь одинаковое внутреннее представление. Это означает, что передача (void *) 0 вместо (char *) 0 не будет проблемой в примере execl()».

    Может ли компилятор привести (void *) 0 в execl(prog, arg, (void*) 0) к "нулевому указателю соответствующего типа" сейчас?

    Почему это противоречит цитате в моем пункте 2?

  4. Если я заменим (void *) 0 в execl(prog, arg, (void*) 0) на приведение 0 к указателю любого типа, например (int *) 0, может ли компилятор привести (int *) 0 в execl(prog, arg, (int*) 0) к «нулевому указателю соответствующего тип"? Спасибо.

  5. Для вызова невариантной функции, например, в sigaction(SIGINT, &sa, (int*) 0), может ли компилятор привести (int *) 0 к «нулевому указателю соответствующего типа»?

Спасибо.

Ответы [ 4 ]

0 голосов
/ 01 апреля 2019

AFAI понимает, что продвижение аргумента по умолчанию применяется к функциям var-args за исключением указателей, которые остаются как есть (пример zwol также поддерживает 1 ). Поэтому, когда 0 передается в виде функций типа var-args, например, семейство exec () , оно распознается как неукрашенное целое число 0 вместо нулевого указателя. En passant, execl(prog, arg, 0); может работать в системах, в которых внутреннее представление нулевого указателя и целое внутреннее представление 0 совпадают, но это не обязательно.

execl(prog, arg, NULL); может также непреднамеренно работать при условии одного из следующих действий

  • Если NULL определено в системе как целое число 0, то вышеупомянутое объяснение прилагается.
  • Если NULL определено в системе как константа нулевого указателя как (void*)0, хотя в переменной функции компилятор не может быть приведен (void*)0 на нулевой указатель соответствующего типа , даже если C стандарты говорят, что char* и void* должны иметь одинаковый внутренний представление.

Для более подробной информации, посмотрите здесь . Дополнительный пример из здесь .


1 Например, execl не получит нулевой указатель от этого 0 на любом ABI, где int составляет 32 бита, char * составляет 64 бита и int количества не передаются со знаком или нулем до 64 битов, когда передаются как часть списка переменных аргументов.

0 голосов
/ 06 сентября 2018

1) Требует ли стандарт C, чтобы нулевой указатель был представлен так же, как (char*) 0

Да, так как константа нулевого указателя имеет тип void *, и потому что void * и char * имеют одинаковое представление.

Это подробно описано в разделе 6.3.2.3p3 стандарта C :

Целочисленное константное выражение со значением 0 или такое выражение, приведенное к типу void *, называется константой нулевого указателя.

А, раздел 6.2.5p28:

Указатель на void должен иметь то же представление и требования выравнивания как указатель на тип символа. 48)

...

48) Те же требования к представлению и выравниванию подразумевал взаимозаменяемость в качестве аргументов функций, возвращать значения из функций и членов союзов.


2) Разве (void *) 0 не является нулевым указателем типа? Если да, то почему компилятор приведёт (void *) 0 в execl(prog, arg, (void*) 0) к "нулю" указатель соответствующего типа "?

Это нулевой указатель типа, и этот тип void *.

Определение execl:

int execl(const char *path, const char *arg, ...);

Таким образом, он не может привести третий параметр к соответствующему типу, потому что он не знает, что такое соответствующий тип, но это не имеет значения, потому что void * и char * являются взаимозаменяемыми для 6.2.4p28 и сноски. 48, как указано выше.

3) Может ли компилятор привести (void *) 0 в execl(prog, arg, (void*) 0) в «нулевой указатель соответствующего типа» сейчас? Почему это противоречит цитате в моей точке 2?

Он по-прежнему не может быть разыгран, потому что не знает, что это за подходящий тип. Но опять же, это не имеет значения, потому что void * и char * взаимозаменяемы.

4) Если я заменю (void *) 0 в execl(prog, arg, (void*) 0) на приведение 0 к указателю любого типа, например (int *) 0, может ли компилятор привести (int *) 0 в execl(prog, arg, (int*) 0) к «нулевому указателю соответствующего типа»?

Нет, потому что, опять же, он не знает, каков соответствующий тип. И в этом случае у вас могут возникнуть проблемы, если int * и char * не имеют одинакового представления.

5) Может ли компилятор приводить (int *) 0 к «нулевому указателю соответствующего типа» при вызове невариантных функций, например, в sigaction(SIGINT, &sa, (int*) 0)?

Да, потому что (int *)0 является нулевым указателем и потому что нулевой указатель может быть преобразован в любой другой указатель.

0 голосов
/ 06 сентября 2018

Начиная с C99, спецификация va_arg читается частично

Если [тип, переданный va_arg в качестве аргумента], несовместим с типом фактического следующего аргумента (как продвигается в соответствии с продвижением аргументов по умолчанию), поведение не определено, за исключением следующих случаев:

  • один тип является целочисленным типом со знаком, другой тип является соответствующим целочисленным типом без знака, и значение представимо в обоих типах;
  • один тип является указателем на void, а другой - указателем на тип символа.

Вторая точка маркера означает, что для любой переменной функции, которая использует va_arg для доступа к своим аргументам, вызов вида

variadic_function("a", "b", "c", (void *)0);

будет действовать всякий раз, когда

variadic_function("a", "b", "c", (char *)0);

было бы.

К сожалению, есть одна загвоздка: я не могу найти никаких требований для переменной стандартной библиотеки функций 1 , чтобы [вести себя так, как будто они] получают доступ к своим аргументам, делая серия звонков на va_arg. Вы, наверное, думаете, ну, как еще они собираются это сделать? На практике это va_arg или рукописный язык ассемблера, и, возможно, комитет не хотел требовать, чтобы рукописный язык ассемблера был идеально эквивалентом, но я не стал бы беспокоиться об этом.

Так что книга, которую вы цитируете, технически неверна. Впрочем, я бы все равно написал

execl(prog, arg, (char *) NULL);

если я собирался использовать NULL в первую очередь (я обычно предпочитаю использовать 0 в качестве константы нулевого указателя), потому что вы не должны писать код, который полагается на расширение NULL до ((void *)0) и

execl(prog, arg, 0);

, безусловно, неверно. Например, execl не получит нулевой указатель от этого 0 на любом ABI, где int - 32 бита, char * - 64 бита, а значения int не расширены до нуля или знака до 64 биты, когда передаются как часть списка аргументов переменной.


1 execl не является частью стандарта C , но является частью стандарта POSIX, и любая система, обеспечивающая execl, в первую очередь, вероятно, соответствует по крайней мере с подмножеством POSIX. Можно предположить, что все из пункта 7.1.4 стандарта C применимо и к функциям, указанным в POSIX.

0 голосов
/ 06 сентября 2018

Во-первых, компилятор не "приводит" ни при каких обстоятельствах. Приведение - это синтаксическая конструкция в исходном коде, которая запрашивает преобразование.

Я предполагаю, что когда вы говорите о «приведении к компилятору», вы имеете в виду говорить о неявном преобразовании , которое представляет собой процесс, посредством которого значение одного типа может быть преобразовано в значение другого типа, без оператор приведения.

Стандарт точно определяет контексты, в которых может применяться неявное преобразование; всегда должен быть целевой тип. Например, в коде int x = Y; выражение Y может быть некоторого типа, который не является int, но имеет неявное преобразование в int.

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

Обычная нить вашего вопроса, похоже, состоит в том, что компилятор должен каким-то образом притвориться, что execl ведет себя так, как если бы был прототип для последнего аргумента. Но на самом деле это не так, и компилятор не имеет магического поведения для определенных функций. То, что вы передаете, это то, что вы получаете.


  1. Стандарт определяет, что значение выражения (char *)0 является нулевым указателем. Он ничего не говорит о представлении нулевых указателей, и может быть несколько разных представлений, которые все являются нулевыми указателями.

  2. Спецификация функции execl говорит, что список аргументов должен заканчиваться (char *)0, который является значением типа char *. Значение типа void * не является значением типа char *, и в этом контексте нет неявных преобразований, как обсуждалось выше.

  3. По-прежнему нет неявного преобразования; В цитируемом вами тексте говорится, что в этой конкретной ситуации вы можете использовать неверный аргумент типа (без параметра прототипа; ожидается char *, но предоставлено void * или наоборот).

  4. Это было бы неопределенным поведением, текст, который вы цитировали в пункте 3, не относится к int *.

  5. Функция sigaction имеет прототип; рассматриваемый параметр struct sigaction *oldact. Когда вы пытаетесь инициализировать параметр прототипа (или любую переменную) значением другого типа, делается попытка неявного преобразования в тип параметра. Существует неявное преобразование любого значения нулевого указателя в значение нулевого указателя другого типа. Это правило содержится в C11 6.3.2.3/4. Так что код в порядке.

...