Qual é o "tipo" de dados que os ponteiros mantêm na linguagem C?


30

Eu sei que os ponteiros contêm endereços. Eu sei que os tipos de ponteiros são "geralmente" conhecidos com base no "tipo" de dados para os quais eles apontam. Porém, os ponteiros ainda são variáveis ​​e os endereços que eles mantêm devem ter um "tipo" de dados. Segundo as minhas informações, os endereços estão no formato hexadecimal. Mas ainda não sei que tipo de dados é esse hexadecimal. (Observe que eu sei o que é um hexadecimal, mas quando você diz 10CBA20, por exemplo, essa sequência de caracteres é? Inteiros? O quê? Quando eu quero acessar o endereço e manipulá-lo ... em si, preciso conhecer seu tipo. é por isso que estou perguntando.)


17
Ponteiros não são variáveis , mas valores . As variáveis ​​mantêm valores (e se seu tipo é um tipo de ponteiro, esse valor é um ponteiro e pode ser o endereço de uma zona de memória contendo algo significativo). Uma determinada zona de memória pode ser usada para armazenar vários valores de tipos diferentes.
Basile Starynkevitch

29
"os endereços estão no formato hexadecimal" Não, isso é apenas o bit do depurador ou da biblioteca que formata. Com o mesmo argumento, você poderia dizer que eles estão em binário ou octal.
usr

É melhor você perguntar sobre o formato , não o tipo . Daí algumas respostas fora de pista abaixo ... (embora as de Kilian sejam exatas).
Lightness Races com Monica

11
Acho que a questão mais profunda aqui é a compreensão do tipo pelo OP . Quando se trata disso, os valores que você está manipulando no seu programa são apenas bits de memória. Tipos são a maneira do programador de dizer ao compilador como tratar esses bits quando ele gera código de montagem.
Justin Lardinois

Suponho que seja tarde demais para editá-lo com todas essas respostas, mas essa pergunta teria sido melhor se você tivesse restringido o hardware e / ou o sistema operacional, por exemplo "no x64 Linux".
Hyde

Respostas:


64

O tipo de uma variável de ponteiro é .. ponteiro.

As operações que você está formalmente autorizado a fazer em C são compará-lo (com outros ponteiros, ou o valor NULL / zero especial), adicionar ou subtrair números inteiros ou convertê-lo em outros ponteiros.

Depois de aceitar um comportamento indefinido , você pode ver qual é realmente o valor. Geralmente, é uma palavra de máquina, o mesmo tipo de número inteiro e geralmente pode ser convertida sem perdas para e de um tipo inteiro. (Muitos códigos do Windows fazem isso ocultando os ponteiros nos typedefs DWORD ou HANDLE).

Existem algumas arquiteturas em que os ponteiros não são simples porque a memória não é plana. DOS / 8086 'próximo' e 'distante'; Espaços diferentes de memória e código do PIC.


2
Você também pode fazer a diferença entre dois ponteiros p1-p2. O resultado é um valor integral assinado. Em particular, #&(array[i])-&(array[j]) == i-j
MSalters

13
Na verdade, a conversão para um tipo integral também é especificada, especificamente para intptr_te uintptr_tcom garantia de ser "grande o suficiente" para valores de ponteiro.
Matthieu M.

3
Você pode depender da conversão para funcionar, mas o mapeamento entre números inteiros e ponteiros é definido pela implementação. (A única exceção é 0 -> nulo, e mesmo que só é especificado se o 0 é um IIRC constante.)
Chao

7
A adição do pespecificador ao printf faz com que a obtenção de uma representação legível por humanos de um ponteiro nulo seja um comportamento definido, se dependente da implementação, em c.
dmckee

6
Essa resposta tem a idéia geralmente correta, mas falha nas reivindicações específicas. Coagir um ponteiro a um tipo integral não é um comportamento indefinido, e os tipos de dados do Windows HANDLE não são valores de ponteiro (eles não são ponteiros ocultos em tipos de dados integrais, são números inteiros ocultos em tipos de ponteiro, para evitar aritmética).
Ben Voigt

44

Você está complicando demais as coisas.

Endereços são apenas números inteiros, ponto final. Idealmente, eles são o número da célula de memória referenciada (na prática, isso fica mais complicado por causa de segmentos, memória virtual etc.).

A sintaxe hexadecimal é uma ficção completa que existe apenas para a conveniência dos programadores. 0x1A e 26 são exatamente o mesmo número do mesmo tipo e nem o que o computador usa - internamente, o computador sempre usa 00011010 (uma série de sinais binários).

Se um compilador permite ou não tratar ponteiros como números depende da definição de linguagem - as linguagens de "programação de sistemas" são tradicionalmente mais transparentes sobre como as coisas funcionam sob o capô, enquanto as linguagens de "alto nível" costumam tentar ocultar o bare metal do programador - mas isso não muda nada sobre o fato de os ponteiros serem números e, geralmente, o tipo de número mais comum (aquele com tantos bits quanto a arquitetura do processador).


26
Os endereços são mais definitivamente não apenas números inteiros. Assim como os números de ponto flutuante definitivamente não são apenas números inteiros.
precisa saber é o seguinte

8
De fato. O contra-exemplo mais conhecido é o Intel 8086, em que os ponteiros são dois números inteiros.
precisa saber é o seguinte

5
@Rob Em um modelo de memória segmentada, um ponteiro pode ser um valor único (um endereço relativo ao início do segmento; um deslocamento) com o segmento implícito, ou um par segmento / seletor e deslocamento . (Acho que a Intel usou o termo "seletor"; tenho preguiça de procurá-lo.) No 8086, eles eram representados como dois números inteiros de 16 bits, que combinavam para formar um endereço físico de 20 bits. (Sim, você poderia endereçar a mesma célula de memória de muitas e muitas maneiras diferentes, se quisesse: endereço = (segmento << 4 + deslocamento) & 0xfffff.) Isso foi transmitido por todos os compatíveis com x86 ao executar em modo real.
um CVn

4
Como programador de assembler de longo prazo, posso atestar que a memória do computador não passa de locais de memória contendo números inteiros. No entanto, é como você os trata e é importante acompanhar o que esses números inteiros representam. Por exemplo, no meu sistema, o número decimal 4075876853 é armazenado como x'F2F0F1F5 ', que é a string' 2015 'em EBCDIC. O decimal de 2015 seria armazenado como 000007DF, enquanto x'0002015C 'representa decimal de 2015 no formato decimal compactado. Como programador de assembler, você precisa acompanhar isso; o compilador faz isso para linguagens HL.
Steve Ives

7
Os endereços podem ser colocados em um-para-um correspondência com números inteiros, mas isso pode tudo o resto em um computador :)
hobbs

16

Um ponteiro é apenas isso - um ponteiro. Não é outra coisa. Não tente pensar que é outra coisa.

Em linguagens como C, C ++ e Objective-C, os ponteiros de dados têm quatro tipos de valores possíveis:

  1. Um ponteiro pode ser o endereço de um objeto.
  2. Um ponteiro pode apontar para além do último elemento de uma matriz.
  3. Um ponteiro pode ser um ponteiro nulo, o que significa que não está apontando para nada.
  4. Um ponteiro pode ter um valor indeterminado, em outras palavras, é lixo e tudo pode acontecer (incluindo coisas ruins) se você tentar usá-lo.

Também existem ponteiros de função, que identificam uma função ou são ponteiros de função nulos ou têm um valor indeterminado.

Outros ponteiros são "ponteiro para membro" em C ++. Estes são mais definitivamente não endereços de memória! Em vez disso, eles identificam um membro de qualquer instância de uma classe. No Objective-C, você tem seletores, que são algo como "ponteiro para um método de instância com um nome de método e nomes de argumento". Como um ponteiro de membro, ele identifica todos os métodos de todas as classes, desde que pareçam iguais.

Você pode investigar como um compilador específico implementa ponteiros, mas essa é uma pergunta totalmente diferente.


4
Existem indicadores para funções e, em C ++, indicadores para membros.
sdenham

Ponteiros C ++ para membros não são endereços de memória? Claro que são. class A { public: int num; int x; }; int A::*pmi = &A::num; A a; int n = a.*pmi;A variável pminão seria muito útil se não contivesse um endereço de memória, a saber, como a última linha do código estabelece, o endereço do membro numda instância ada classe A. Você pode converter isso em um intponteiro comum (embora o compilador provavelmente lhe dê um aviso) e desreferenciá-lo com êxito (provando que é um açúcar sintático para qualquer outro ponteiro).
Dodgethesteamroller

9

Um ponteiro é um padrão de bits que endereça (identificando exclusivamente para fins de leitura ou gravação) uma palavra de armazenamento na RAM. Por razões históricas e convencionais, a unidade de atualização é de oito bits, conhecida em inglês como "byte" ou em francês, mais logicamente, como um octeto. Isso é onipresente, mas não inerente; outros tamanhos existem.

Se bem me lembro, havia um computador que usava uma palavra de 29 bits; não é apenas uma potência de dois, é também primordial. Eu pensei que era SILLIAC, mas o artigo pertinente da Wikipedia não suporta isso. O CAN BUS usa endereços de 29 bits, mas, por convenção, os endereços de rede não são referidos como ponteiros, mesmo quando são funcionalmente idênticos.

As pessoas continuam afirmando que os ponteiros são números inteiros. Isso não é intrínseco nem essencial, mas se interpretarmos os padrões de bits como números inteiros, a qualidade útil da ordinalidade emerge, permitindo a implementação muito direta (e, portanto, eficiente em hardware pequeno) de construções como "string" e "array". A noção de memória contígua depende da adjacência ordinal, e o posicionamento relativo é possível; comparação de números inteiros e operações aritméticas podem ser aplicadas significativamente. Por esse motivo, quase sempre existe uma forte correlação entre o tamanho da palavra para endereçamento de armazenamento e a ALU (o que faz matemática inteira).

Às vezes os dois não correspondem. Nos primeiros PCs, o barramento de endereços tinha 24 bits de largura.


Atualmente, o Nitpick, em sistemas operacionais comuns, identifica o local na memória virtual e não tem nada a ver diretamente com uma palavra física da RAM (o local da memória virtual pode nem existir fisicamente se estiver em uma página de memória conhecida como zeros pelo sistema operacional )
Hyde

@hyde - Seu argumento tem mérito no contexto em que você obviamente o pretendia, mas a forma dominante do computador é o controlador incorporado, onde maravilhas como a memória virtual são inovações recentes com implantação limitada. Além disso, o que você apontou não ajuda o OP a entender os ponteiros. Eu pensei que algum contexto histórico tornaria tudo muito menos arbitrário.
quer

Não sei se falar sobre RAM ajudará o OP a entender, pois a pergunta é especificamente sobre o que realmente são os ponteiros . Ah, outro nitpick, no ponteiro c, por definição, aponta para byte (pode ser convertido com segurança para, char*por exemplo, para fins de cópia / comparação de memória e sizeof char==1conforme definido pelo padrão C), não palavra (a menos que o tamanho da palavra da CPU seja igual ao tamanho do byte).
Hyde

O que os ponteiros são fundamentalmente são as chaves de hash para armazenamento. Isso é invariável no idioma e na plataforma.
quer

A questão é sobre c ponteiros . E ponteiros definitivamente não são chaves de hash, porque não há tabela de hash, nem algoritmo de hash. Eles naturalmente são algum tipo de chave de mapa / dicionário (para definição suficientemente ampla de "mapa"), mas não chaves de hash .
Hyde

6

Basicamente, todo computador moderno é uma máquina que empurra pouco. Geralmente ele empurra bits em grupos de dados, chamados bytes, palavras, dwords ou qwords.

Um byte consiste em 8 bits, uma palavra 2 bytes (ou 16 bits), uma palavra dword 2 (ou 32 bits) e uma palavra qword 2 dwords (ou 64 bits). Essa não é a única maneira de organizar bits. A manipulação de 128 e 256 bits também ocorre, geralmente nas instruções do SIMD.

As instruções de montagem operam nos registradores e os endereços de memória geralmente operam em uma das formas acima.

A ALU (unidades lógicas aritméticas) opera em pacotes de bits como se representassem números inteiros (geralmente no formato Complemento de Dois) e FPUs como se estivessem em valores de ponto flutuante (geralmente no estilo IEEE 754 floate double). Outras partes agirão como se fossem dados agrupados de algum formato, caracteres, entradas da tabela, instruções da CPU ou endereços.

Em um computador típico de 64 bits, pacotes de 8 bytes (64 bits) são endereços. Exibimos esses endereços convencionalmente como em um formato hexadecimal (como 0xabcd1234cdef5678), mas essa é apenas uma maneira fácil para os humanos lerem os padrões de bits. Cada byte (8 bits) é escrito como dois caracteres hexadecimais (equivalentemente, cada caractere hexadecimal - 0 a F - representa 4 bits).

O que realmente está acontecendo (para alguns níveis) é que existem bits, geralmente armazenados em um registro ou armazenados em locais adjacentes em um banco de memória, e estamos apenas tentando descrevê-los para outro ser humano.

Seguir um ponteiro consiste em pedir ao controlador de memória que nos forneça alguns dados nesse local. Você normalmente solicita ao controlador de memória um certo número de bytes em um determinado local (bem, implicitamente, um intervalo de locais, geralmente contíguo), e ele é entregue através de vários mecanismos nos quais não vou entrar.

O código geralmente especifica um destino para os dados a serem buscados - um registro, outro endereço de memória etc. - e geralmente é uma má idéia carregar dados de ponto flutuante em um registro esperando um número inteiro ou vice-versa.

O tipo de dados em C / C ++ é algo que o compilador controla e altera o código gerado. Geralmente, não há nada intrínseco nos dados que os torne realmente de qualquer tipo. Apenas uma coleção de bits (organizados em bytes) que são manipulados como um número inteiro (ou como um flutuador ou como um endereço) pelo código.

Há exceções para isto. Existem arquiteturas em que certas coisas são de tipos diferentes de bits. O exemplo mais comum são as páginas de execução protegidas - enquanto as instruções informando à CPU o que fazem são bits, no tempo de execução, as páginas (de memória) contendo o código a ser executado são marcadas especialmente, não podem ser modificadas e você não pode executar páginas que não estão marcadas. como páginas de execução.

Também existem dados somente leitura (às vezes armazenados na ROM que não podem ser gravados fisicamente!), Problemas de alinhamento (alguns processadores não podem carregar doubles da memória, a menos que estejam alinhados de maneiras particulares, ou instruções SIMD que exijam certo alinhamento) e inúmeras outras peculiaridades da arquitetura.

Até o nível de detalhe acima é uma mentira. Os computadores não estão "realmente" pressionando bits, estão pressionando tensões e correntes. Essas tensões e correntes às vezes não fazem o que "deveriam" fazer no nível de abstração dos bits. Os chips são projetados para detectar a maioria desses erros e corrigi-los sem que a abstração de nível superior precise estar ciente disso.

Até isso é mentira.

Cada nível de abstração oculta o nível abaixo e permite que você pense em resolver problemas sem ter que se lembrar dos diagramas de Feynman para imprimir "Hello World".

Portanto, com um nível suficiente de honestidade, os computadores pressionam os bits, e esses bits recebem significado pela maneira como são usados.


3

As pessoas fizeram uma grande diferença se os ponteiros são inteiros ou não. Na verdade, existem respostas para essas perguntas. No entanto, você terá que dar um passo na terra das especificações, o que não é para os fracos de coração. Vamos dar uma olhada na especificação C, ISO / IEC 9899: TC2

6.3.2.3 Ponteiros

  1. Um número inteiro pode ser convertido para qualquer tipo de ponteiro. Exceto conforme especificado anteriormente, o resultado é definido pela implementação, pode não estar alinhado corretamente, pode não apontar para uma entidade do tipo referenciado e pode ser uma representação de interceptação.

  2. Qualquer tipo de ponteiro pode ser convertido em um tipo inteiro. Exceto conforme especificado anteriormente, o resultado é definido pela implementação. Se o resultado não puder ser representado no tipo inteiro, o comportamento será indefinido. O resultado não precisa estar no intervalo de valores de nenhum tipo inteiro.

Agora, você precisará conhecer alguns termos comuns de especificações. "Implementação definida" significa que todo compilador pode defini-lo de maneira diferente. De fato, um compilador pode até defini-lo de maneiras diferentes, dependendo das suas configurações. Comportamento indefinido significa que o compilador pode fazer absolutamente qualquer coisa, desde dar um erro no tempo de compilação a comportamentos inexplicáveis, até funcionar perfeitamente.

A partir disso, podemos ver que o formulário de armazenamento subjacente não está especificado, exceto que pode haver uma conversão para um tipo inteiro. Agora, verdade seja dita, praticamente todo compilador sob o sol representa ponteiros sob o capô como endereços inteiros (com alguns casos especiais em que pode ser representado como 2 inteiros em vez de apenas 1), mas a especificação permite absolutamente qualquer coisa, como representar endereços como uma sequência de 10 caracteres!

Se sairmos rapidamente do C e examinarmos as especificações do C ++, obteremos um pouco mais de clareza reinterpret_cast, mas essa é uma linguagem diferente; portanto, o valor para você pode variar:

ISO / IEC N337: rascunho de especificação C ++ 11 (só tenho o rascunho em mãos)

5.2.10 Reinterpretar elenco

  1. Um ponteiro pode ser explicitamente convertido em qualquer tipo integral grande o suficiente para retê-lo. A função de mapeamento é definida pela implementação. [Nota: Ele não é surpreendente para quem conhece a estrutura de endereçamento da máquina subjacente. —End note] Um valor do tipo std :: nullptr_t pode ser convertido em um tipo integral; a conversão tem o mesmo significado e validade que uma conversão de (vazio *) 0 para o tipo integral. [Nota: Um reinterpret_cast não pode ser usado para converter um valor de qualquer tipo no tipo std :: nullptr_t. - end note]

  2. Um valor do tipo integral ou do tipo de enumeração pode ser explicitamente convertido em um ponteiro. Um ponteiro convertido em um número inteiro de tamanho suficiente (se houver algum na implementação) e retornado ao mesmo tipo de ponteiro terá seu valor original; mapeamentos entre ponteiros e números inteiros são definidos pela implementação. [Nota: Exceto conforme descrito em 3.7.4.3, o resultado dessa conversão não será um valor de ponteiro derivado com segurança. - end note]

Como você pode ver aqui, com mais alguns anos, o C ++ descobriu que era seguro assumir que existia um mapeamento para números inteiros; portanto, não se fala mais em comportamento indefinido (embora exista uma contradição interessante entre as partes 4 e 5 com o fraseado "se houver algum na implementação")


Agora, o que você deve tirar disso?

  • A representação exata dos ponteiros é a implementação definida. (na verdade, apenas para torná-lo mais confuso, alguns pequenos computadores incorporados representam o ponteiro nulo, (vazio ) 0, como endereço 255 para suportar alguns truques de alias de endereço que eles usam) *
  • Se você precisar perguntar sobre a representação de ponteiros na memória, provavelmente não está em um ponto da sua carreira de programação em que deseja brincar com eles.

A melhor aposta: converter para um (caractere *). As especificações C e C ++ estão cheias de regras que especificam o empacotamento de matrizes e estruturas, e ambas sempre permitem a conversão de qualquer ponteiro para um caractere *. char é sempre 1 byte (não garantido em C, mas, pelo C ++ 11, ele se tornou uma parte obrigatória da linguagem, portanto, é relativamente seguro assumir que é de 1 byte em todos os lugares). Isso permite que você faça alguma aritmética de ponteiro no nível de byte a byte sem precisar realmente conhecer as representações específicas da implementação dos ponteiros.


Você pode necessariamente converter um ponteiro de função para a char *? Estou pensando em uma máquina hipotética com espaços de endereço separados para código e dados.
Philip Kendall

@PhilipKendall Good point. Não incluí essa parte da especificação, mas os ponteiros de função são tratados como algo completamente diferente dos ponteiros de dados na especificação, devido exatamente ao problema que você levanta. Ponteiros membros também são tratados de forma diferente (mas eles também atuam de forma muito diferente também)
Cort Ammon - Reintegrar Monica

A charé sempre 1 byte em C. Citando o padrão C: "O operador sizeof produz o tamanho (em bytes) de seu operando" e "Quando sizeof é aplicado a um operando que possui o tipo char, char não assinado ou char assinado, (ou uma versão qualificada), o resultado é 1. " Talvez você esteja pensando que um byte tem 8 bits. Isso não é necessariamente o caso. Para estar em conformidade com o padrão, um byte deve conter pelo menos 8 bits.
David Hammen

A especificação descreve a conversão entre tipos de ponteiro e número inteiro. Deve-se sempre lembrar que uma "conversão" entre tipos não implica uma igualdade dos tipos, nem mesmo que uma representação binária dos dois tipos na memória tenha o mesmo padrão de bits. (ASCII pode ser "convertido" para EBCDIC Big-endian pode ser "convertido" a little-endian Etc...)
user2338816

1

Na maioria das arquiteturas, o tipo de ponteiro deixa de existir depois de traduzido em código de máquina (exceto talvez "ponteiros gordos"). Portanto, um ponteiro para um intseria indistinguível de um ponteiro para um double, pelo menos por si só. *

[*] Embora você ainda possa fazer suposições com base nos tipos de operações que você aplica a ele.


1

Uma coisa importante a entender sobre C e C ++ é que tipos são realmente. Tudo o que eles realmente fazem é indicar ao compilador como interpretar um conjunto de bits / bytes. Vamos começar com o seguinte código:

int var = -1337;

Dependendo da arquitetura, um número inteiro geralmente recebe 32 bits de espaço para armazenar esse valor. Isso significa que o espaço na memória em que var está armazenado será semelhante a "11111111 11111111 11111010 11000111" ou em hexadecimal "0xFFFFFAC7". É isso aí. Isso é tudo o que é armazenado nesse local. Todos os tipos fazem é dizer ao compilador como interpretar essas informações. Ponteiros não são diferentes. Se eu fizer algo assim:

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

Em seguida, o compilador obterá a localização de var e, em seguida, armazene esse endereço da mesma maneira que o primeiro trecho de código salva o valor -1337. Não há diferença em como eles são armazenados, apenas em como eles são usados. Nem importa que eu tenha feito var_ptr um ponteiro para um int. Se você quisesse, você poderia fazer.

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

Isso copiará o valor hexadecimal acima de var (0xFFFFFAC7) no local que armazena o valor de var2. Se usarmos var2, descobriríamos que o valor seria 4294965959. Os bytes em var2 são iguais a var, mas o valor numérico é diferente. O compilador as interpretou de maneira diferente, porque dissemos que esses bits representam um longo tempo sem sinal. Você também pode fazer o mesmo com o valor do ponteiro.

unsigned int var3 = (unsigned int)var_ptr;

Você acabaria interpretando o valor que representa o endereço de var como um int não assinado neste exemplo.

Espero que isso esclareça as coisas para você e ofereça uma melhor compreensão de como o C funciona. Observe que você NÃO DEVE fazer nenhuma das coisas loucas que eu fiz nas duas linhas abaixo no código de produção real. Isso foi apenas para demonstração.


1

Inteiro.

O espaço de endereço em um computador é numerado seqüencialmente, iniciando em 0 e incrementado em 1. Portanto, um ponteiro retém um número inteiro que corresponde a um endereço no espaço de endereço.


1

Tipos combinam.

Em particular, certos tipos se combinam, quase como se tivessem sido parametrizados com espaços reservados. Os tipos de matriz e ponteiro são assim; eles têm um espaço reservado, que é o tipo do elemento da matriz ou a coisa apontada, respectivamente. Os tipos de função também são assim; eles podem ter vários espaços reservados para os parâmetros e um espaço reservado para o tipo de retorno.

Uma variável declarada para conter um ponteiro para char possui o tipo "ponteiro para char". Uma variável declarada para manter um ponteiro para ponteiro para int possui o tipo "ponteiro para ponteiro para int".

Um (valor do) tipo "ponteiro para ponteiro para int" pode ser alterado para "ponteiro para int" por uma operação de desreferência. Portanto, a noção de tipo não é apenas palavras, mas uma construção matematicamente significativa, ditando o que podemos fazer com valores do tipo (como desreferência, ou passar como parâmetro ou atribuir a variável; também determina o tamanho (contagem de bytes) de operações de indexação, aritmética e incremento / decremento).

PS Se você quiser se aprofundar nos tipos, tente este blog: http://www.goodmath.org/blog/2015/05/13/expressions-and-arity-part-1/

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.