O ponteiro
refere -
se a uma célula de memória e
desreferenciar um ponteiro significa ler o valor da célula especificada. O valor do ponteiro em si é o endereço da célula de memória. O padrão da linguagem C não especifica o formulário para representar os endereços de memória. Este é um ponto muito importante, pois diferentes arquiteturas podem usar diferentes modelos de endereçamento. A maioria das arquiteturas modernas usa espaço de endereço linear ou similar. No entanto, mesmo essa questão não é especificada estritamente, pois os endereços podem ser físicos ou virtuais. Algumas arquiteturas usam uma representação não numérica. Portanto, o Symbolics Lisp Machine opera com tuplas do formulário
(objeto, deslocamento) como endereços.
Algum tempo depois, após a publicação da tradução em Habré, o autor fez grandes modificações no texto do artigo. Atualizar uma tradução no Habré não é uma boa ideia, pois alguns comentários perdem o significado ou ficam fora de lugar. Não quero publicar o texto como um novo artigo. Portanto, acabamos de atualizar a tradução do artigo no viva64.com, e aqui deixamos tudo como está. Se você é um novo leitor, sugiro que leia uma tradução mais recente em nosso site, clicando no link acima. |
O padrão não estipula a forma de apresentação dos ponteiros, mas estipula - em maior ou menor grau - as operações com eles. Abaixo, consideramos essas operações e os recursos de sua definição no padrão. Vamos começar com o seguinte exemplo:
#include <stdio.h> int main(void) { int a, b; int *p = &a; int *q = &b + 1; printf("%p %p %d\n", (void *)p, (void *)q, p == q); return 0; }
Se compilarmos esse código GCC com o nível de otimização 1 e executarmos o programa no Linux x86-64, ele imprimirá o seguinte:
0x7fff4a35b19c 0x7fff4a35b19c 0
Observe que os ponteiros
peq se referem ao mesmo endereço. No entanto, o resultado da expressão
p == q é
falso , e isso à primeira vista parece estranho. Dois ponteiros para o mesmo endereço não devem ser iguais?
Veja como o padrão C define o resultado da verificação de dois ponteiros quanto à igualdade:
C11 § 6.5.9, n.o 6
Dois ponteiros são iguais se, e somente se, ambos são zero, apontam para o mesmo objeto (incluindo um ponteiro para o objeto e o primeiro subobjeto no objeto) ou uma função, ou apontam para a posição após o último elemento da matriz ou um ponteiro refere-se à posição após o último elemento da matriz e a outra refere-se ao início de outra matriz imediatamente após a primeira no mesmo espaço de endereço. |
Antes de tudo, surge a pergunta: o que é um "objeto
" ? Como estamos falando da linguagem C, é óbvio que aqui os objetos não têm nada a ver com objetos nas linguagens OOP como C ++. No padrão C, esse conceito não está completamente definido:
C11 § 3.15
Um objeto é uma área de armazenamento de tempo de execução cujo conteúdo pode ser usado para representar valores
NOTA Quando mencionado, um objeto pode ser considerado como tendo um tipo específico; ver 6.3.2.1. |
Vamos acertar. Uma variável inteira de 16 bits é um conjunto de dados na memória que pode representar valores inteiros de 16 bits. Portanto, essa variável é um objeto. Dois ponteiros serão iguais se um deles se referir ao primeiro byte de um número inteiro e o segundo ao segundo byte do mesmo número? O comitê de padronização de idiomas, é claro, não quis dizer isso. Mas aqui deve-se notar que, a esse respeito, ele não tem explicações claras e somos forçados a adivinhar o que realmente significava.
Quando o compilador atrapalha
Vamos voltar ao nosso primeiro exemplo. O ponteiro
p é obtido no objeto
ae o ponteiro
q é do objeto
b . No segundo caso, a aritmética do endereço é usada, definida para os operadores de mais e menos da seguinte maneira:
C11 § 6.5.6, cláusula 7
Quando usado com esses operadores, um ponteiro para um objeto que não é um elemento da matriz se comporta como um ponteiro para o início de uma matriz com o comprimento de um elemento, cujo tipo corresponde ao tipo do objeto original. |
Como qualquer ponteiro para um objeto que não seja uma matriz
realmente se torna um ponteiro para uma matriz com o comprimento de um elemento, o padrão define a aritmética do endereço apenas para ponteiros para matrizes - esse é o ponto 8. Estamos interessados na seguinte parte:
C11 § 6.5.6, cláusula 8
Se uma expressão inteira for adicionada ou subtraída do ponteiro, o ponteiro resultante será do mesmo tipo que o ponteiro original. Se o ponteiro de origem se referir a um elemento da matriz e a matriz tiver comprimento suficiente, a fonte e os elementos resultantes serão separados um do outro, para que a diferença entre seus índices seja igual ao valor da expressão inteira. Em outras palavras, se a expressão P apontar para o i-ésimo elemento da matriz, as expressões (P) + N (ou seu equivalente N + (P) ) e (P) -N (onde N tem o valor n) indicarão respectivamente (i + n) th e (i - n) th elementos da matriz, desde que existam. Além disso, se a expressão P apontar para o último elemento da matriz, a expressão (P) +1 indica a posição após o último elemento da matriz, e se a expressão Q indica a posição após o último elemento da matriz, a expressão (Q) -1 indica o último elemento array. Se a fonte e os ponteiros resultantes fizerem referência a elementos da mesma matriz ou à posição após o último elemento da matriz, o estouro será excluído; caso contrário, o comportamento é indefinido. Se o ponteiro resultante se referir à posição após o último elemento da matriz, o operador unário * não poderá ser aplicado a ele. |
Daqui resulta que o resultado da expressão
& b + 1 deve ser definitivamente um endereço e, portanto,
peq são indicadores válidos. Deixe-me lembrá-lo como a igualdade de dois ponteiros no padrão é definida: "
Dois ponteiros são iguais se e somente [...] se um ponteiro se refere à posição após o último elemento da matriz e o outro ao início de outra matriz imediatamente após o primeiro na mesma endereço " (C11 § 6.5.9, cláusula 6). É exatamente isso que observamos em nosso exemplo. O ponteiro q se refere à posição após o objeto b, imediatamente seguido pelo objeto a, ao qual o ponteiro p se refere. Então, existe um bug no GCC? Essa contradição foi descrita em 2014 como
bug # 61502 , mas os desenvolvedores do GCC não a consideram um bug e, portanto, não vão corrigi-lo.
Um problema semelhante foi encontrado em 2016 pelos programadores do Linux. Considere o seguinte código:
extern int _start[]; extern int _end[]; void foo(void) { for (int *i = _start; i != _end; ++i) { } }
Os símbolos
_start e
_end especificam os limites da área de memória. Como eles são transferidos para um arquivo externo, o compilador não sabe como as matrizes estão realmente localizadas na memória. Por esse motivo, ele deve ser cuidadoso aqui e prosseguir com a suposição de que eles se seguem no espaço de endereço. No entanto, o GCC compila a condição do loop para que seja sempre verdadeira, o que torna o loop infinito. Esse problema é descrito aqui neste
post no LKML - um fragmento de código semelhante é usado lá. Parece que, nesse caso, os autores do GCC, no entanto, levaram em conta os comentários e alteraram o comportamento do compilador. Pelo menos não consegui reproduzir esse erro no GCC versão 7.3.1 no Linux x86_64.
Solução - no relatório de bug # 260?
Nosso caso pode esclarecer o relatório de erro
# 260 . É mais sobre valores incertos, mas você pode encontrar um comentário curioso do comitê:
As implementações do compilador [...] também podem distinguir ponteiros obtidos de diferentes objetos, mesmo se esses ponteiros tiverem o mesmo conjunto de bits.Se considerarmos esse comentário literalmente, é lógico que o resultado da expressão
p == q seja “falso”, pois
peq são obtidos de diferentes objetos que não estão conectados de forma alguma. Parece que estamos nos aproximando da verdade - ou não? Até agora, lidamos com operadores de igualdade, mas e os operadores de relações?
A pista final está em relação aos operadores?
A definição dos operadores de relacionamento
< ,
<= ,
> e
> = no contexto de comparações de ponteiros contém um pensamento curioso:
C11 § 6.5.8 n.o 5
O resultado da comparação de dois ponteiros depende da posição relativa dos objetos indicados no espaço de endereço. Se dois ponteiros para tipos de objetos se referirem ao mesmo objeto, ou ambos se referem à posição após o último elemento da mesma matriz, esses ponteiros são iguais. Se os objetos indicados forem membros do mesmo objeto composto, os ponteiros para os membros da estrutura declarados posteriormente serão mais do que os ponteiros para os membros declarados anteriormente e os ponteiros para os elementos de uma matriz com índices mais altos serão mais do que os ponteiros para os elementos da mesma matriz com os índices mais baixos. Todos os ponteiros para membros da mesma associação são iguais. Se a expressão P apontar para um elemento da matriz e a expressão Q apontar para o último elemento da mesma matriz, o valor da expressão indicadora Q + 1 será maior que o valor da expressão P. Em todos os outros casos, o comportamento não está definido. |
De acordo com essa definição, o resultado da comparação de ponteiros é determinado apenas se os ponteiros forem obtidos do
mesmo objeto. Mostramos isso com dois exemplos.
int *p = malloc(64 * sizeof(int)); int *q = malloc(64 * sizeof(int)); if (p < q)
Aqui, os ponteiros
peq se referem a dois objetos diferentes que não estão interconectados. Portanto, o resultado de sua comparação não está definido. Mas no exemplo a seguir:
int *p = malloc(64 * sizeof(int)); int *q = p + 42; if (p < q) foo();
ponteiros
peq se referem ao mesmo objeto e, portanto, estão interconectados. Portanto, eles podem ser comparados - a menos que o
malloc retorne um valor nulo.
Sumário
O padrão C11 não descreve adequadamente comparações de ponteiros. O ponto mais problemático que encontramos foi o parágrafo 6 § 6.5.9, onde é explicitamente permitido comparar dois ponteiros que referenciam duas matrizes diferentes. Isso contradiz o comentário do relatório de erro # 260. No entanto, estamos falando de significados indefinidos, e eu não gostaria de construir meu raciocínio com base apenas nesse comentário e interpretá-lo em outro contexto. Ao comparar ponteiros, os operadores de relacionamento são definidos de maneira ligeiramente diferente dos operadores de igualdade - ou seja, os operadores de relacionamento são definidos apenas se os dois ponteiros forem obtidos do
mesmo objeto.
Se ignorarmos o texto do padrão e perguntarmos se é possível comparar dois indicadores obtidos de dois objetos diferentes, em qualquer caso, a resposta provavelmente será "não". O exemplo no início do artigo demonstra um problema teórico. Como as variáveis
aeb têm durações automáticas de armazenamento, nossas suposições sobre sua colocação na memória não são confiáveis. Em alguns casos, podemos adivinhar, mas é óbvio que esse código não pode ser portado com segurança, e você pode descobrir o significado do programa apenas compilando e executando ou desmontando o código, e isso contradiz qualquer paradigma de programação sério.
No entanto, em geral, não estou satisfeito com a redação do padrão C11 e, como várias pessoas já encontraram esse problema, a pergunta permanece: por que não formular as regras com mais clareza?
Adição
Ponteiros para a posição após o último elemento da matriz
Quanto à regra sobre comparação e endereçamento aritmético de ponteiros para a posição após o último elemento da matriz, muitas vezes você pode encontrar exceções. Suponha que o padrão não permita comparar dois ponteiros obtidos da
mesma matriz, mesmo que pelo menos um deles se refira à posição além do final da matriz. O código a seguir não funcionaria:
const int num = 64; int x[num]; for (int *i = x; i < &x[num]; ++i) { }
Usando um loop, percorremos todo o array
x , composto por 64 elementos, ou seja, o corpo do loop deve ser executado exatamente 64 vezes. Mas, de fato, a condição é verificada 65 vezes - uma vez mais que o número de elementos na matriz. Nas primeiras 64 iterações, o ponteiro
i sempre se refere ao interior da matriz
x , enquanto a expressão
& x [num] sempre indica a posição após o último elemento da matriz. Na 65ª iteração, o ponteiro
i também se referirá à posição além do final da matriz
x , por causa da qual a condição do loop se torna falsa. Essa é uma maneira conveniente de ignorar toda a matriz e se baseia em uma exceção à regra da incerteza no comportamento ao comparar esses indicadores. Observe que o padrão descreve apenas o comportamento ao comparar ponteiros; a desreferenciação é uma questão separada.
É possível mudar nosso exemplo para que nenhum ponteiro se refira à posição após o último elemento da matriz
x ? É possível, mas será mais difícil. Teremos que alterar a condição do loop e proibir o incremento da variável
i na última iteração.
const int num = 64; int x[num]; for (int *i = x; i <= &x[num-1]; ++i) { if (i == &x[num-1]) break; }
Este código está cheio de sutilezas técnicas, as quais perturbam a distração da tarefa principal. Além disso, um ramo adicional apareceu no corpo do loop. Portanto, acho razoável que o padrão permita exceções ao comparar ponteiros de posição após o último elemento de uma matriz.
Nota da equipe do PVS-StudioAo desenvolver o analisador de código PVS-Studio, às vezes precisamos lidar com questões sutis para tornar o diagnóstico mais preciso ou fornecer consultas detalhadas aos nossos clientes. Este artigo nos pareceu interessante, pois aborda questões em que nós mesmos não nos sentimos totalmente confiantes. Por isso, pedimos ao autor para publicar sua tradução. Esperamos que mais programadores de C e C ++ a conheçam e entendam que não é tão simples e que, quando o analisador repentinamente exibir uma mensagem estranha, não se apresse em considerá-la um falso positivo :).O artigo foi publicado pela primeira vez em inglês em stefansf.de. As traduções são publicadas com permissão do autor.