2009-04-03 16 views
30

En ANSI C, offsetof se define a continuación.¿Por qué funciona esta implementación de offsetof()?

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

¿Por qué no generará un error de segmentación ya que estamos desreferenciando un puntero NULL? ¿O se trata de una especie de truco de compilación en el que se ve que solo se saca la dirección del desplazamiento, por lo que calcula estáticamente la dirección sin desreferenciarla? ¿Este código también es portátil?

+2

¿Es esta la primera pregunta que he visto en SO quejándose del código que funciona? :-) – paxdiablo

+2

Hubo ese tipo con if (0) {asm (nop)} donde dejarlo fuera hizo que algo fallara ... – RBerteig

+3

ANSI C (en realidad ISO C) no especifica esta definición para 'offsetof'. Simplemente especifica cómo debe comportarse. La definición real corresponde a cada implementación y puede variar una implementación a otra. –

Respuesta

32

En ningún punto en el código anterior se desreferencia nada. Se produce una desreferencia cuando se usa * o -> en un valor de dirección para encontrar el valor al que se hace referencia. El único uso de * anterior se encuentra en una declaración de tipo con el fin de emitir.

El operador -> se usa arriba, pero no se usa para acceder al valor. En cambio, se usa para captar la dirección del valor. Aquí está un ejemplo de código no-macro que debe hacerlo un poco más claro

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

La segunda línea en realidad no causa una falta de referencia (dependiente de la implementación). Simplemente devuelve la dirección de SomeIntMember dentro del valor pSomeType.

Lo que ves es una gran cantidad de conversión entre tipos arbitrarios y punteros de caracteres. El motivo de char es que es uno de los únicos tipos (quizás el único) en el estándar C89 que tiene un tamaño explícito. El tamaño es 1. Al asegurar que el tamaño es uno, el código anterior puede hacer la magia maligna de calcular el verdadero desplazamiento del valor.

+0

No tengo un estándar C disponible, pero pensé que recordaba algo en C90 acerca de que no necesariamente se podía usar (no solo desreferencia) direcciones arbitrarias. El razonamiento eran máquinas como la 8086 y la IBM 370 que usaban registros de segmentos, y no podían referirse a todo el espacio de direcciones. –

+0

En el estándar C, '->' en '& (pSomeType-> SomeIntMember)' causa una desreferencia. Tal vez podría aclarar a qué se refería cuando afirma que no. –

2

No segfault porque no está desreferenciando. La dirección del puntero se utiliza como un número que se resta de otro número, no se utiliza para abordar operaciones de memoria.

2

Calcula el desplazamiento del miembro m relativo a la dirección de inicio de la representación de un objeto de tipo st.

((st *)(0)) se refiere a un puntero NULL del tipo st *. &((st *)(0))->m se refiere a la dirección del miembro m en este objeto. Como la dirección de inicio de este objeto es 0 (NULL), la dirección del miembro m es exactamente el desplazamiento.

char * conversión y la diferencia calcula el desplazamiento en bytes. Según las operaciones del puntero, cuando se hace una diferencia entre dos punteros del tipo T *, el resultado es la cantidad de objetos del tipo T representados entre las dos direcciones contenidas en los operandos.

+0

Sean, ¿por qué se necesitaba esa resta? ¿no podemos simplemente devolver (char *) y ((st *) (0)) -> m? – chappar

+0

Creo que la resta no es realmente necesaria, pero no estoy 100% seguro ... –

+0

Existen implementaciones C para las cuales un puntero nulo no está representado por el valor 0 internamente. En dicha implementación, supongo que este código C fallará por completo porque el compilador no sabrá cómo manejar el puntero nulo en la aritmética del puntero, o puede funcionar gracias a la resta (porque la representación del puntero nulo necesita ser cancelado). – vinc17

8

En ANSI C, offsetof NO se define así. Una de las razones por las que no se define así es que algunos entornos emitirán excepciones de punteros nulos o se bloquearán de otras formas. Por lo tanto, ANSI C deja abierta la implementación de offsetof() para compiladores.

El código que se muestra arriba es típico para compiladores/entornos que no comprueban activamente los punteros NULL, pero solo fallan cuando se leen bytes desde un puntero NULL.

+0

Para que quede claro, la macro 'offsetof()' se ha implementado muy comúnmente como se muestra en la pregunta, o incluso más simplemente sin la resta, en la gran mayoría de las plataformas donde los punteros son efectivamente enteros. La mayoría de los compiladores C no comprueban activamente los punteros NULL. La expresión utilizada hace ** NO ** desreferencia * cualquier cosa --- --- simplemente calcula el desplazamiento utilizando una dirección (que pasa a ser cero) con una simple suma aritmética de la compensación internamente conocida del miembro. Cuando está optimizado, ni siquiera se realiza ninguna adición en tiempo de ejecución. –

6

Para responder a la última parte de la pregunta, el código no es portátil.

El resultado de restar dos punteros se define y es portátil solo si los dos punteros apuntan a objetos en la misma matriz o señalan uno pasado al último objeto de la matriz (7.6.2 Aditivo operadores, H & S Quinta Edición)

7

A pesar de que es una implementación típica de offsetof, no es un mandato de la norma, que se limita a decir:

los siguientes tipos y macros se definen en el encabezado estándar <stddef.h> [...]

offsetof(type,member-designator)

que se expande a una expresión constante entera que tiene el tipo size_t, el valor de que es el desplazamiento en bytes, el elemento de estructura (designado por member-designator), desde el comienzo de su estructura (designados por type). El tipo y el miembro de designador deberá ser tal que, dada

statictypet;

entonces la expresión &(t.member-designator) evalúa a una constante de dirección. (Si el miembro especificado no es un campo de bits, el comportamiento no está definido.)

Leer PJ Plauger de "La biblioteca estándar de C" para una discusión de la misma y los otros artículos en <stddef.h> que son todas las funciones de la línea fronteriza que podría (¿debería?) estar en el lenguaje adecuado y que podría requerir un soporte especial para el compilador.

Es de interés histórico solamente, pero utilicé un compilador ANSI C temprano en 386/IX (véase, ya le dije de interés histórico, alrededor de 1990) que se estrelló en esa versión de offsetof pero funcionó cuando lo revisé a:

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

Eso fue un error del compilador de clases, entre otras cosas porque la cabecera se distribuyó con el compilador y no funcionaba.

1

Listado 1: un conjunto representativo de offsetof() definiciones de macro

// 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)); 
} 

Los diversos operadores de la macro se evalúan en un orden tal que los siguientes pasos se realizan:

  1. ((s *)0) toma el número entero cero y lo arroja como un puntero al s.
  2. ((s *)0)->m desreferencias que puntero apuntan al miembro de estructura m.
  3. &(((s *)0)->m) calcula la dirección de m.
  4. (size_t)&(((s *)0)->m) arroja el resultado a un tipo de datos apropiado.

Por definición, la propia estructura reside en la dirección 0.Se deduce que la dirección del campo señalado (Paso 3 anterior) debe ser la compensación, en bytes, desde el inicio de la estructura.

Cuestiones relacionadas