¿Cuál es el "tipo" de datos que contienen los punteros en el lenguaje C?


30

Sé que los punteros tienen direcciones. Sé que los tipos de punteros son "generalmente" conocidos en función del "tipo" de datos que señalan. Pero, los punteros siguen siendo variables y las direcciones que contienen deben tener un "tipo" de datos. Según mi información, las direcciones están en formato hexadecimal. Pero, todavía no sé qué "tipo" de datos es este hexadecimal. (Tenga en cuenta que sé lo que es un hexadecimal, pero cuando dice 10CBA20, por ejemplo, ¿esta cadena de caracteres es? Enteros? ¿Qué? Cuando quiero acceder a la dirección y manipularla ... en sí misma, necesito saber su tipo. Esto es por eso que estoy preguntando)


17
Los punteros no son variables , sino valores . Las variables contienen valores (y si su tipo es un tipo de puntero, ese valor es un puntero y podría ser la dirección de una zona de memoria que contiene algo significativo). Una zona de memoria dada podría usarse para contener varios valores de diferentes tipos.
Basile Starynkevitch

29
"las direcciones están en formato hexadecimal" No, eso es solo el depurador o los bits de formato de una biblioteca. Con el mismo argumento se podría decir que están en binario u octal.
usr

Sería mejor preguntar sobre el formato , no el tipo . Por lo tanto, algunas respuestas fuera de pista a continuación ... (aunque Kilian es perfecto).
Lightness compite con Monica el

1
Creo que el problema más profundo aquí es la comprensión del tipo por parte de OP . Cuando se trata de eso, los valores que está manipulando en su programa son solo bits en la memoria. Los tipos son la forma en que el programador le dice al compilador cómo tratar esos bits cuando genera código de ensamblaje.
Justin Lardinois

Supongo que es demasiado tarde para editarlo con todas esas respuestas, pero esta pregunta hubiera sido mejor si hubiera restringido el hardware y / o el sistema operativo, por ejemplo "en Linux x64".
hyde

Respuestas:


64

El tipo de una variable de puntero es ... puntero.

Las operaciones que se le permiten formalmente en C son compararlo (con otros punteros, o el valor especial NULL / zero), sumar o restar enteros, o lanzarlo a otros punteros.

Una vez que acepte un comportamiento indefinido , puede ver cuál es realmente el valor. Por lo general, será una palabra de máquina, el mismo tipo de cosa que un número entero, y generalmente se puede convertir sin pérdida hacia y desde un tipo entero. (Una gran cantidad de código de Windows hace esto ocultando punteros en DWORD o HANDLE typedefs).

Hay algunas arquitecturas donde los punteros no son simples porque la memoria no es plana. DOS / 8086 'cerca' y 'lejos'; Diferentes espacios de memoria y código de PIC.


2
También se le permite tomar la diferencia entre dos punteros p1-p2. El resultado es un valor integral firmado. En particular,&(array[i])-&(array[j]) == i-j
MSalters

13
En realidad, también se especifica la conversión a un tipo integral, específicamente a intptr_ty uintptr_tque se garantiza que son "lo suficientemente grandes" para los valores de puntero.
Matthieu M.

3
Puede depender de la conversión al trabajo, pero la asignación entre enteros y punteros está definida por la implementación. (La única excepción es 0 -> nulo, e incluso eso solo se especifica si el 0 es un IIRC constante.)
cHao

77
La adición del pespecificador a printf hace que obtener una representación legible por humanos de un puntero nulo sea un comportamiento definido, si la implementación depende de c.
dmckee

66
Esta respuesta tiene la idea generalmente correcta, pero falla en las afirmaciones específicas. La coacción de un puntero a tipo integral no es un comportamiento indefinido, y los tipos de datos HANDLE de Windows no son valores de puntero (no son punteros ocultos en tipos de datos integrales, son enteros ocultos en tipos de puntero, para evitar la aritmética).
Ben Voigt

44

Estás complicando demasiado las cosas.

Las direcciones son solo enteros, punto. Idealmente, son el número de la celda de memoria referenciada (en la práctica, esto se vuelve más complicado debido a los segmentos, la memoria virtual, etc.).

La sintaxis hexadecimal es una ficción completa que existe solo para la conveniencia de los programadores. 0x1A y 26 son exactamente el mismo número de exactamente el mismo tipo , y tampoco es lo que usa la computadora: internamente, la computadora siempre usa 00011010 (una serie de señales binarias).

Si un compilador le permite o no tratar los punteros como números depende de la definición del lenguaje: los lenguajes de "programación de sistemas" son tradicionalmente más transparentes sobre cómo funcionan las cosas bajo el capó, mientras que los lenguajes de "alto nivel" a menudo intentan ocultar el metal desnudo. del programador, pero eso no cambia nada sobre el hecho de que los punteros son números, y generalmente el tipo de número más común (el que tiene tantos bits como la arquitectura de su procesador).


26
Las direcciones definitivamente no son solo enteros. Así como los números de coma flotante definitivamente no son solo enteros.
gnasher729

8
En efecto. El contraejemplo más conocido es el Intel 8086, donde los punteros son dos enteros.
MSalters

55
@Rob En un modelo de memoria segmentada, un puntero puede ser un solo valor (una dirección relativa al inicio del segmento; un desplazamiento) con el segmento implícito, o un segmento / selector y par de desplazamiento . (Creo que Intel usó el término "selector"; soy demasiado vago para buscarlo). En el 8086, estos se representaron como dos enteros de 16 bits, que se combinaron para formar una dirección física de 20 bits. (Sí, podría direccionar la misma celda de memoria de muchas, muchas maneras diferentes, si estuviera tan inclinado: dirección = (segmento << 4 + desplazamiento) y 0xfffff.) Esto se trasladó a través de todas las x86 compatibles cuando se ejecuta en modo real.
un CVn

44
Como programador de ensamblador a largo plazo, puedo dar fe de que la memoria de la computadora no es más que ubicaciones de memoria que contienen números enteros. Sin embargo, lo importante es cómo los trata y hacer un seguimiento de lo que representan esos enteros. Por ejemplo, en mi sistema, el número decimal 4075876853 se almacena como x'F2F0F1F5 ', que es la cadena' 2015 'en EBCDIC. El decimal 2015 se almacenaría como 000007DF, mientras que x'0002015C 'representa el decimal 2015 en formato decimal empaquetado. Como programador de ensamblador, debe realizar un seguimiento de estos; el compilador lo hace para los idiomas HL.
Steve Ives

77
Las direcciones se pueden poner en correspondencia uno a uno con enteros, pero también todo lo demás en una computadora :)
hobbs

16

Un puntero es solo eso: un puntero. No es otra cosa. No intentes pensar que es otra cosa.

En lenguajes como C, C ++ y Objective-C, los punteros de datos tienen cuatro tipos de valores posibles:

  1. Un puntero puede ser la dirección de un objeto.
  2. Un puntero puede apuntar más allá del último elemento de una matriz.
  3. Un puntero puede ser un puntero nulo, lo que significa que no apunta a nada.
  4. Un puntero puede tener un valor indeterminado, en otras palabras, es basura, y cualquier cosa puede suceder (incluidas las cosas malas) si intenta usarlo.

También hay punteros de función, que identifican una función o son punteros de función nula o tienen un valor indeterminado.

Otros punteros son "puntero a miembro" en C ++. ¡Estas definitivamente no son direcciones de memoria! En cambio, identifican a un miembro de cualquier instancia de una clase. En Objective-C, tiene selectores, que son algo así como "puntero a un método de instancia con un nombre de método y nombres de argumento". Al igual que un puntero de miembro, identifica todos los métodos de todas las clases siempre que se vean iguales.

Puede investigar cómo un compilador específico implementa punteros, pero esa es una pregunta completamente diferente.


44
Hay punteros a funciones y, en C ++, punteros a miembros.
sdenham

¿Los apuntadores de C ++ a los miembros no son direcciones de memoria? Claro que lo son. class A { public: int num; int x; }; int A::*pmi = &A::num; A a; int n = a.*pmi;La variable pmino sería muy útil si no contuviera una dirección de memoria, es decir, como establece la última línea del código, la dirección del miembro numde la instancia ade la clase A. Podría lanzar esto a un intpuntero ordinario (aunque el compilador probablemente le daría una advertencia) y desreferenciarlo con éxito (demostrando que es azúcar sintáctico para cualquier otro puntero).
dodgethesteamroller

9

Un puntero es un direccionamiento de patrón de bits (que identifica de manera única con el propósito de leer o escribir) una palabra de almacenamiento en RAM. Por razones históricas y convencionales, la unidad de actualización es de ocho bits, conocida en inglés como "byte" o en francés, más lógicamente, como un octeto. Esto es ubicuo pero no inherente; Otros tamaños han existido.

Si no recuerdo mal, había una computadora que usaba una palabra de 29 bits; no solo no es una potencia de dos, es incluso primo. Pensé que esto era SILLIAC, pero el artículo pertinente de Wikipedia no lo admite. CAN BUS utiliza direcciones de 29 bits pero, por convención, las direcciones de red no se denominan punteros, incluso cuando son funcionalmente idénticas.

La gente sigue afirmando que los punteros son enteros. Esto no es intrínseco ni esencial, pero si interpretamos los patrones de bits como enteros, emerge la calidad útil de la ordinalidad, lo que permite la implementación muy directa (y, por lo tanto, eficiente en hardware pequeño) de construcciones como "string" y "array". La noción de memoria contigua depende de la adyacencia ordinal, y es posible el posicionamiento relativo; La comparación de enteros y las operaciones aritméticas se pueden aplicar de manera significativa. Por esta razón, casi siempre existe una fuerte correlación entre el tamaño de palabra para el direccionamiento de almacenamiento y la ALU (lo que hace matemática entera).

A veces los dos no se corresponden. En las primeras PC, el bus de direcciones tenía 24 bits de ancho.


Nitpick, en estos días en sistemas operativos comunes, el puntero identifica la ubicación en la memoria virtual y no tiene nada que ver directamente con una palabra RAM física (la ubicación de la memoria virtual podría no existir físicamente si está en una página de memoria que el sistema operativo sabe que son todos ceros )
hyde

@hyde: su argumento tiene mérito en el contexto en el que obviamente lo pretendió, pero la forma dominante de la computadora es el controlador integrado, donde las maravillas como la memoria virtual son innovaciones recientes con una implementación limitada. Además, lo que ha señalado de ninguna manera ayuda al OP a comprender los punteros. Pensé que algún contexto histórico lo haría todo mucho menos arbitrario.
Peter Wone

No sé si hablar de RAM ayudará a OP a comprender, ya que la pregunta es específicamente sobre qué son realmente los punteros . Oh, otro punto crítico, en el puntero c, por definición, apunta al byte (se puede convertir de manera segura, por char*ejemplo, para copiar / comparar la memoria, y sizeof char==1según lo define el estándar C), no palabra (a menos que el tamaño de palabra de la CPU sea el mismo que el tamaño de byte).
hyde

Lo que los punteros son fundamentalmente son las claves hash para el almacenamiento. Este es el lenguaje y la plataforma invariable.
Peter Wone

La pregunta es sobre c punteros . Y los punteros definitivamente no son claves hash, porque no hay una tabla hash, ni un algoritmo hash. Naturalmente, son algún tipo de teclas de mapa / diccionario (para una definición suficientemente amplia de "mapa"), pero no teclas hash .
hyde

6

Básicamente, cada computadora moderna es una máquina de empuje de bits. Por lo general, empuja bits en grupos de datos, llamados bytes, palabras, dwords o qwords.

Un byte consta de 8 bits, una palabra de 2 bytes (o 16 bits), una palabra d de 2 palabras (o 32 bits) y una palabra q de 2 dwords (o 64 bits). Estas no son la única forma de organizar los bits. También se produce manipulación de 128 bits y 256 bits, a menudo en las instrucciones SIMD.

Las instrucciones de ensamblaje operan en registros y las direcciones de memoria generalmente operan en una de las formas anteriores.

Las ALU (unidades lógicas aritméticas) operan en paquetes de bits como si representaran números enteros (generalmente el formato Complemento de dos) y las FPU como si tuvieran valores de coma flotante (generalmente estilo IEEE 754 floatydouble ). Otras partes actuarán como si fueran datos agrupados de algún formato, caracteres, entradas de tabla, instrucciones de CPU o direcciones.

En una computadora típica de 64 bits, los paquetes de 8 bytes (64 bits) son direcciones. Mostramos estas direcciones convencionalmente como en un formato hexadecimal (como0xabcd1234cdef5678 ), pero esa es solo una manera fácil para que los humanos lean los patrones de bits. Cada byte (8 bits) se escribe como dos caracteres hexadecimales (de manera equivalente, cada carácter hexadecimal - 0 a F - representa 4 bits).

Lo que realmente está sucediendo (para cierto nivel) es que hay bits, generalmente almacenados en un registro o almacenados en ubicaciones adyacentes en un banco de memoria, y solo estamos tratando de describirlos a otro humano.

Seguir un puntero consiste en pedirle al controlador de memoria que nos brinde algunos datos en esa ubicación. Por lo general, le pediría al controlador de memoria un cierto número de bytes en una ubicación determinada (bueno, implícitamente un rango de ubicaciones, generalmente contiguas), y se entrega a través de varios mecanismos en los que no entraré.

El código generalmente especifica un destino para los datos que se van a buscar (un registro, otra dirección de memoria, etc.) y, por lo general, es una mala idea cargar datos de punto flotante en un registro esperando un número entero, o viceversa.

El tipo de datos en C / C ++ es algo que el compilador realiza un seguimiento y cambia el código que se genera. Por lo general, no hay nada intrínseco en los datos que lo haga realmente de cualquier tipo. Solo una colección de bits (organizados en bytes) que el código manipula de forma entera (o de forma flotante, o de dirección).

Existen excepciones para esto. Hay arquitecturas donde ciertas cosas son un tipo diferente de bits. El ejemplo más común son las páginas de ejecución protegidas: mientras que las instrucciones que le dicen a la CPU qué hacen son bits, en el tiempo de ejecución, las páginas (de memoria) que contienen código para ejecutar están marcadas especialmente, no pueden modificarse y no puede ejecutar páginas que no están marcadas como páginas de ejecución.

También hay datos de solo lectura (a veces almacenados en ROM que no se pueden escribir físicamente), problemas de alineación (algunos procesadores no se pueden cargar double s de la memoria a menos que estén alineados de manera particular, o instrucciones SIMD que requieren cierta alineación), y una miríada de Otras peculiaridades de la arquitectura.

Incluso el nivel de detalle anterior es una mentira. Las computadoras no están "realmente" presionando bits, realmente están presionando los voltajes y la corriente. Estos voltajes y corrientes a veces no hacen lo que se supone que deben hacer en el nivel de abstracción de bits. Los chips están diseñados para detectar la mayoría de estos errores y corregirlos sin que la abstracción de nivel superior tenga que ser consciente de ello.

Incluso eso es una mentira.

Cada nivel de abstracción oculta el siguiente y le permite pensar en resolver problemas sin tener que tener en cuenta los diagramas de Feynman para imprimir "Hello World" .

Entonces, con un nivel suficiente de honestidad, las computadoras empujan bits, y esos bits tienen significado por cómo se usan.


3

La gente se ha preocupado mucho de si los punteros son enteros o no. En realidad hay respuestas a estas preguntas. Sin embargo, tendrá que dar un paso hacia la tierra de las especificaciones, que no es para los débiles de corazón. Vamos a echar un vistazo a la especificación C, ISO / IEC 9899: TC2

6.3.2.3 Punteros

  1. Un entero puede convertirse a cualquier tipo de puntero. Excepto como se especificó anteriormente, el resultado está definido por la implementación, podría no estar correctamente alineado, podría no apuntar a una entidad del tipo referenciado y podría ser una representación trampa.

  2. Cualquier tipo de puntero se puede convertir a un tipo entero. Excepto como se especificó anteriormente, el resultado está definido por la implementación. Si el resultado no puede representarse en el tipo entero, el comportamiento es indefinido. El resultado no necesita estar en el rango de valores de ningún tipo de entero.

Ahora para esto, necesitará conocer algunos términos de especificaciones comunes. "implementación definida" significa que cada compilador puede definirlo de manera diferente. De hecho, un compilador puede incluso definirlo de diferentes maneras dependiendo de la configuración de su compilador. El comportamiento indefinido significa que el compilador puede hacer absolutamente cualquier cosa, desde dar un error de tiempo de compilación hasta comportamientos inexplicables, hasta trabajar perfectamente.

A partir de esto, podemos ver que el formulario de almacenamiento subyacente no está especificado, aparte de que puede haber una conversión a un tipo entero. Ahora bien, la verdad es que prácticamente todos los compiladores bajo el sol representan punteros debajo del capó como direcciones enteras (con un puñado de casos especiales en los que podría representarse como 2 enteros en lugar de solo 1), pero la especificación permite absolutamente cualquier cosa, como representar direcciones como una cadena de 10 caracteres!

Si avanzamos rápidamente fuera de C y miramos las especificaciones de C ++, obtenemos un poco más de claridad reinterpret_cast, pero este es un lenguaje diferente, por lo que su valor para usted puede variar:

ISO / IEC N337: especificación de borrador de C ++ 11 (solo tengo el borrador a mano)

5.2.10 Reinterpretar elenco

  1. Un puntero se puede convertir explícitamente a cualquier tipo integral lo suficientemente grande como para contenerlo. La función de mapeo está definida por la implementación. [Nota: no pretende sorprender a quienes conocen la estructura de direccionamiento de la máquina subyacente. —Nota final] Un valor de tipo std :: nullptr_t se puede convertir a un tipo integral; la conversión tiene el mismo significado y validez que una conversión de (void *) 0 al tipo integral. [Nota: un reinterpret_cast no se puede usar para convertir un valor de ningún tipo al tipo std :: nullptr_t. —Nota final]

  2. Un valor de tipo integral o tipo de enumeración se puede convertir explícitamente en un puntero. Un puntero convertido a un entero de tamaño suficiente (si existe alguno en la implementación) y volver al mismo tipo de puntero tendrá su valor original; las asignaciones entre punteros y enteros se definen de otra manera en la implementación. [Nota: Excepto como se describe en 3.7.4.3, el resultado de dicha conversión no será un valor de puntero derivado de forma segura. —Nota final]

Como puede ver aquí, con unos pocos años más en su haber, C ++ descubrió que era seguro suponer que existía un mapeo de enteros, por lo que ya no se habla de comportamiento indefinido (aunque existe una contradicción interesante entre las partes 4 y 5 con la frase "si existe en la implementación")


¿Ahora qué deberías quitar de esto?

  • La representación exacta de los punteros está definida por la implementación. (de hecho, solo para hacerlo más desordenado, algunas pequeñas computadoras integradas representan el puntero nulo, (nulo ) 0, como la dirección 255 para admitir algunos trucos de alias de direcciones que usan) *
  • Si tiene que preguntar sobre la representación de los punteros en la memoria, probablemente no esté en un punto de su carrera de programación en el que quiera jugar con ellos.

La mejor apuesta: lanzar a un (char *). Las especificaciones C y C ++ están llenas de reglas que especifican el empaquetado de matrices y estructuras, y ambas siempre permiten la conversión de cualquier puntero a un char *. char siempre tiene 1 byte (no está garantizado en C, pero en C ++ 11 se ha convertido en una parte obligatoria del lenguaje, por lo que es relativamente seguro asumir que es 1 byte en todas partes). Esto le permite hacer una aritmética de puntero a nivel byte por byte sin tener que recurrir a la necesidad de conocer las representaciones específicas de implementación de los punteros.


¿Se puede convertir necesariamente un puntero de función en a char *? Estoy pensando en una máquina hipotética con espacios de direcciones separados para código y datos.
Philip Kendall

@PhilipKendall Buen punto. No incluí esa parte de la especificación, pero los punteros de función se tratan como algo completamente diferente a los punteros de datos en la especificación debido exactamente al problema que plantea. Punteros miembros también son tratados de manera diferente (pero que también actúan de manera muy diferente también)
Cort Amón - Restablecer Mónica

A charsiempre es 1 byte en C. Citando del estándar C: "El operador sizeof produce el tamaño (en bytes) de su operando" y "Cuando sizeof se aplica a un operando que tiene un tipo char, unsigned char o signo char, (o una versión calificada del mismo) el resultado es 1. " Quizás esté pensando que un byte tiene 8 bits de longitud. Ese no es necesariamente el caso. Para cumplir con el estándar, un byte debe contener al menos 8 bits.
David Hammen

La especificación describe la conversión entre punteros y tipos enteros. Siempre debe tenerse en cuenta que una "conversión" entre tipos no implica una igualdad de los tipos, ni siquiera que una representación binaria de los dos tipos en la memoria tendría el mismo patrón de bits. (ASCII se puede "convertir" a EBCDIC. Big-endian se puede "convertir" a little-endian. Etc.)
user2338816

1

En la mayoría de las arquitecturas, el tipo de puntero deja de existir una vez que se ha traducido al código de máquina (a excepción de quizás "punteros gordos"). Por lo tanto, un puntero a un intsería indistinguible de un puntero a un double, al menos por sí solo. *

[*] Aunque, aún puedes hacer conjeturas en función del tipo de operaciones que le apliques.


1

Una cosa importante para entender sobre C y C ++ es qué tipos son en realidad. Todo lo que realmente hacen es indicarle al compilador cómo interpretar un conjunto de bits / bytes. Comencemos con el siguiente código:

int var = -1337;

Dependiendo de la arquitectura, un entero generalmente recibe 32 bits de espacio para almacenar ese valor. Eso significa que el espacio en la memoria donde se almacena var se verá como "11111111 11111111 11111010 11000111" o en hexadecimal "0xFFFFFAC7". Eso es. Eso es todo lo que se almacena en esa ubicación. Todo lo que hacen es decirle al compilador cómo interpretar esa información. Los punteros no son diferentes. Si hago algo como esto:

int* var_ptr = &var;   //the ampersand is telling C "get the address where var's value is located"

Luego, el compilador obtendrá la ubicación de var, y luego almacenará esa dirección de la misma manera que el primer fragmento de código guarda el valor -1337. No hay diferencia en cómo se almacenan, solo en cómo se usan. Ni siquiera importa que haya hecho var_ptr un puntero a un int. Si quisieras, podrías hacerlo.

unsigned int var2 = *(unsigned int*)var_ptr;

Esto copiará el valor hexadecimal anterior de var (0xFFFFFAC7) en la ubicación que almacena el valor de var2. Si tuviéramos que usar var2, encontraríamos que el valor sería 4294965959. Los bytes en var2 son los mismos que var, pero el valor numérico difiere. El compilador los interpretó de manera diferente porque le dijimos que esos bits representan un largo sin signo. También podría hacer lo mismo para el valor del puntero.

unsigned int var3 = (unsigned int)var_ptr;

Terminaría interpretando el valor que representa la dirección de var como un int sin signo en este ejemplo.

Esperemos que esto te aclare las cosas y te dé una mejor idea de cómo funciona C. Tenga en cuenta que NO DEBE hacer ninguna de las cosas locas que hice en las dos líneas siguientes en el código de producción real. Eso fue solo para demostración.


1

Entero.

El espacio de direcciones en una computadora se numera secuencialmente, comenzando en 0, y se incrementa en 1. Por lo tanto, un puntero contendrá un número entero que corresponde a una dirección en el espacio de direcciones.


1

Los tipos se combinan.

En particular, ciertos tipos se combinan, casi como si estuvieran parametrizados con marcadores de posición. Los tipos de matriz y puntero son así; tienen uno de esos marcadores de posición, que es el tipo del elemento de la matriz o la cosa a la que se apunta, respectivamente. Los tipos de funciones también son así; pueden tener múltiples marcadores de posición para los parámetros y un marcador de posición para el tipo de retorno.

Una variable que se declara que contiene un puntero a char tiene el tipo "puntero a char". Una variable que se declara que contiene un puntero a puntero a int tiene el tipo "puntero a puntero a int".

Un (valor de) tipo "puntero a puntero a int" se puede cambiar a "puntero a int" mediante una operación de desreferencia. Entonces, la noción de tipo no es solo palabras, sino una construcción matemáticamente significativa, que dicta lo que podemos hacer con los valores del tipo (como desreferenciar, pasar como parámetro o asignar a variable; también determina el tamaño (conteo de bytes) de operaciones de indexación, aritmética e incremento / decremento).

PD: si quieres profundizar en los tipos, prueba este blog: http://www.goodmath.org/blog/2015/05/13/expressions-and-arity-part-1/

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.