¿Por qué funciona esta implementación de offsetof()?
pointers (7)
En ANSI C, offsetof se define como a continuación.
#define offsetof(st, m) /
((size_t) ( (char *)&((st *)(0))->m - (char *)0 ))
¿Por qué no arrojará esto 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?
A pesar de que es una implementación típica de offsetof
, no es un requisito del estándar, que solo dice:
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 de enteros que tiene tipo
size_t
, cuyo valor es el desplazamiento en bytes, al miembro de la estructura (designado pormember-designator
), desde el comienzo de su estructura (designado portype
). El tipo y el designador del miembro serán tales que
type
static
t;
luego, la expresión
&(t.
member-designator
)
evalúa a una constante de dirección. (Si el miembro especificado es un campo de bits, el comportamiento no está definido).
Lea "The Standard C Library" de PJ Plauger para una discusión sobre este y los otros elementos en <stddef.h>
que son todas las características de border-line que podrían (¿deberían?) <stddef.h>
en el lenguaje apropiado, y que podrían requerir una compatibilidad especial con el compilador .
Es de interés histórico solamente, pero utilicé un compilador ANSI C temprano en 386 / IX (mira, ya te dije sobre el interés histórico, alrededor de 1990) que se offsetof
en esa versión de offsetof
pero funcionó cuando lo revisé para:
#define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024))
Eso fue una especie de error de compilación, sobre todo porque el encabezado se distribuyó con el compilador y no funcionó.
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))
refiere a un puntero NULL
de tipo st *
. &((st *)(0))->m
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.
conversión de char *
y la diferencia calcula el desplazamiento en bytes. Según las operaciones del puntero, cuando se hace una diferencia entre dos punteros de tipo T *
, el resultado es la cantidad de objetos de tipo T
representados entre las dos direcciones contenidas en los operandos.
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 la implementación de offsetof( )
abierta a los compiladores.
El código que se muestra arriba es típico para los compiladores / entornos que no comprueban activamente los punteros NULL, pero solo fallan cuando los bytes se leen desde un puntero NULL.
En ningún punto en el código anterior se desreferencia nada. Una desreferencia ocurre cuando el *
o ->
se usa en un valor de dirección para encontrar el valor referenciado. El único uso de *
arriba está en una declaración de tipo para el lanzamiento.
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í hay una muestra del código que no es de macro que debería hacerlo un poco más claro
SomeType *pSomeType = GetTheValue();
int* pMember = &(pSomeType->SomeIntMember);
La segunda línea en realidad no causa una desreferencia (depende de la implementación). Simplemente devuelve la dirección de SomeIntMember
dentro del valor pSomeType
.
Lo que se ve es una gran cantidad de conversión entre tipos arbitrarios y punteros de char. 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.
Listado 1: Un conjunto representativo de definiciones de macro 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));
}
Los diversos operadores dentro de la macro se evalúan en un orden tal que se realizan los siguientes pasos:
-
((s *)0)
toma el entero cero y lo lanza como un puntero as
. -
((s *)0)->m
desreferencias que apuntan al miembro de la estructuram
. -
&(((s *)0)->m)
calcula la dirección dem
. -
(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 el desplazamiento, en bytes, desde el inicio de la estructura.
No segfault porque no lo 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.
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 apuntan a uno pasado el último objeto de la matriz (7.6.2 Operadores aditivos, H & S Quinta edición)