Подтвердить что ты не робот

Почему эта реализация offsetof() работает?

В ANSI C значение offset определяется как указано ниже.

#define offsetof(st, m) \
    ((size_t) ( (char *)&((st *)(0))->m - (char *)0 ))

Почему это не приведет к ошибке сегментации, так как мы разыскиваем указатель NULL? Или это какой-то взлом компилятора, где он видит, что выведен только адрес смещения, поэтому он статически вычисляет адрес без фактического разыменования его? Также этот переносимый код?

4b9b3361

Ответ 1

Ни в коем случае в приведенном выше коде ничего не разыменовывается. Разрыв происходит, когда * или -> используется для значения адреса для поиска ссылочного значения. Единственное использование * выше в объявлении типа для целей литья.

Оператор -> используется выше, но он не используется для доступа к значению. Вместо этого он использовал для захвата адрес значения. Вот пример не-макрокода, который должен сделать его более понятным

SomeType *pSomeType = GetTheValue();
int* pMember = &(pSomeType->SomeIntMember);

Вторая строка фактически не вызывает разыменования (зависит от реализации). Он просто возвращает адрес SomeIntMember в пределах значения pSomeType.

То, что вы видите, - это много литья между произвольными типами и указателями char. Причиной для char является то, что он является единственным типом (возможно, единственным) типа C89, который имеет явный размер. Размер равен 1. Обеспечив размер один, приведенный выше код может сделать злую магию вычисления истинного смещения значения.

Ответ 2

В ANSI C offsetof НЕ определяется как это. Одна из причин, почему это не так, заключается в том, что некоторые среды действительно будут вызывать исключения из нулевого указателя или сбой другими способами. Следовательно, ANSI C оставляет реализацию offsetof( ) открытой для компиляторов.

Показанный выше код типичен для компиляторов/сред, которые не активно проверяют указатели NULL, но терпят неудачу только тогда, когда байты считываются из указателя NULL.

Ответ 3

Хотя это типичная реализация offsetof, она не обязана стандартом, который просто говорит:

Следующие типы и макросы определены в стандартном заголовке <stddef.h> [...]

offsetof( type , member-designator )

который расширяется до целочисленного константного выражения, имеющего тип size_t, значение который является смещением в байтах, члену структуры (обозначенному member-designator), от начала его структуры (обозначается type). Обозначение типа и члена должны быть такими, чтобы данный

static type t;

то выражение &(t. member-designator ) оценивается константой адреса. (Если указанным элементом является бит-поле, поведение undefined.)

Прочитайте PJ Plauger "Библиотека стандартного C" для обсуждения его и других элементов в <stddef.h>, которые являются всеми пограничными функциями, которые могли бы (должны?) быть в правильном языке и для чего может потребоваться специальный компилятор поддержка.

Это только исторический интерес, но я использовал ранний компилятор ANSI C на 386/IX (см., я рассказал вам об историческом интересе, около 1990 года), который разбился об этой версии offsetof, но работал, когда я пересмотрел его до

#define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024))

Это была ошибка компилятора, не в последнюю очередь потому, что заголовок был распространен вместе с компилятором и не работал.

Ответ 4

Чтобы ответить на последнюю часть вопроса, код не переносится.

Результат вычитания двух указателей определяется и переносится только в том случае, если два указателя указывают на объекты в том же массиве или указывают на один из последних объектов массива (7.6.2 Аддитивные операторы, H & S Fifth Edition)

Ответ 5

Это не segfault, потому что вы не разыгрываете его. Адрес указателя используется как число, которое вычитается из другого числа, не используется для адресации операций с памятью.

Ответ 6

Он вычисляет смещение члена m относительно начального адреса представления объекта типа st.

((st *)(0)) относится к указателю NULL типа st *. &((st *)(0))->m относится к адресу члена m в этом объекте. Поскольку начальный адрес этого объекта 0 (NULL), адрес члена m является точно смещением.

char *, а разница вычисляет смещение в байтах. В соответствии с операциями указателя, когда вы делаете разницу между двумя указателями типа T *, результатом является количество объектов типа T, представленных между двумя адресами, содержащимися в операндах.

Ответ 7

Листинг 1: Репрезентативный набор макросов offsetof()

// Keil 8051 compiler
#define offsetof(s,m) (size_t)&(((s *)0)->m)

// Microsoft x86 compiler (version 7)
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)

// Diab Coldfire compiler
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))

typedef struct 
{
    int     i;
    float   f;
    char    c;
} SFOO;

int main(void)
{
  printf("Offset of 'f' is %zu\n", offsetof(SFOO, f));
}

Различные операторы в макросе оцениваются в таком порядке, что выполняются следующие шаги:

  • ((s *)0) принимает целое число 0 и выводит его как указатель на s.
  • ((s *)0)->m, где указатель указывает на член структуры m.
  • &(((s *)0)->m) вычисляет адрес m.
  • (size_t)&(((s *)0)->m) приводит результат к соответствующему типу данных.

По определению сама структура находится по адресу 0. Из этого следует, что адрес поля, на который указывает (шаг 3 выше), должен быть смещением в байтах от начала структуры.