Почему эта реализация offsetof () работает? - PullRequest
32 голосов
/ 03 апреля 2009

В ANSI C смещение определяется следующим образом.

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

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

Ответы [ 7 ]

35 голосов
/ 03 апреля 2009

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

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

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

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

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

8 голосов
/ 03 апреля 2009

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

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

offsetof(type,member-designator)

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

statictypet;

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

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

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

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

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

8 голосов
/ 03 апреля 2009

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

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

7 голосов
/ 03 апреля 2009

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

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

2 голосов
/ 03 апреля 2009

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

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

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

2 голосов
/ 03 апреля 2009

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

1 голос
/ 23 июля 2014

Листинг 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));
}

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

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

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

...