Como a comparação de ponteiros funciona em C? Tudo bem comparar ponteiros que não apontam para a mesma matriz?

33

No capítulo 5 de K&R (Linguagem de programação C 2ª edição), li o seguinte:

Primeiro, os ponteiros podem ser comparados sob certas circunstâncias. Se pe qponto aos membros da mesma matriz, relações então, como ==, !=, <, >=, etc. trabalho corretamente.

O que parece implicar que apenas ponteiros apontando para a mesma matriz podem ser comparados.

No entanto, quando eu tentei esse código

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 é impresso na tela.

Primeiro de tudo, pensei que seria indefinido ou algum tipo ou erro, porque pte pxnão estão apontando para a mesma matriz (pelo menos no meu entendimento).

Também é pt > pxporque os dois ponteiros estão apontando para as variáveis ​​armazenadas na pilha e a pilha cresce, então o endereço de memória de té maior que o de x? É por isso que pt > pxé verdade?

Fico mais confuso quando o malloc é trazido. Também em K&R, no capítulo 8.7, está escrito o seguinte:

Ainda existe uma suposição, no entanto, de que ponteiros para diferentes blocos retornados por sbrkpodem ser comparados significativamente. Isso não é garantido pelo padrão, que permite comparações de ponteiros apenas dentro de uma matriz. Portanto, essa versão mallocé portátil apenas entre máquinas para as quais a comparação geral de ponteiros é significativa.

Não tive problema em comparar ponteiros que apontavam para o espaço alocado na pilha com ponteiros que apontavam para empilhar variáveis.

Por exemplo, o código a seguir funcionou bem, com 1a impressão:

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);

Com base em minhas experiências com meu compilador, sou levado a pensar que qualquer ponteiro pode ser comparado a qualquer outro ponteiro, independentemente de onde eles apontem individualmente. Além disso, acho que a aritmética dos ponteiros entre dois ponteiros é boa, não importa para onde eles apontem individualmente, porque a aritmética está apenas usando os endereços de memória que os ponteiros armazenam.

Ainda assim, estou confuso com o que estou lendo em K&R.

A razão pela qual estou perguntando é porque meu professor. realmente fez uma pergunta do exame. Ele deu o seguinte código:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}

O que eles avaliam para:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

A resposta é 0, 1e 0.

(Meu professor inclui a isenção de responsabilidade no exame de que as perguntas são para um ambiente de programação Ubuntu Linux 16.04, versão de 64 bits)

(nota do editor: se o SO permitisse mais tags, essa última parte garantiria , e talvez . Se o ponto da pergunta / classe fosse especificamente detalhes de implementação de SO de baixo nível, em vez de C. portátil)

Shisui
fonte
17
Está talvez confundindo o que é válido em Ccom o que é seguro em C. A comparação de dois ponteiros com o mesmo tipo sempre pode ser feita (verificação da igualdade, por exemplo), no entanto, é possível usar aritmética e comparação do ponteiro >e <só é seguro quando usado em um determinado array (ou bloco de memória).
Adrian Mole
13
Como um aparte, você não deve aprender C com a K&R. Para começar, o idioma passou por muitas mudanças desde então. E, para ser sincero, o código de exemplo era de uma época em que a tezza, em vez da legibilidade, era valorizada.
precisa
5
Não, não é garantido que funcione. Na prática, pode falhar em máquinas com modelos de memória segmentada. Consulte C possui um equivalente a std :: less do C ++? Na maioria das máquinas modernas, isso funcionará apesar do UB.
Peter Cordes
6
@ Adam: Feche, mas na verdade isso é UB (a menos que o compilador que o OP estava usando, GCC, opte por defini-lo. Pode ser). Mas UB não significa "definitivamente explode"; um dos comportamentos possíveis para o UB está funcionando da maneira que você esperava !! É isso que torna o UB tão desagradável; ele pode funcionar corretamente em uma compilação de depuração e falhar com a otimização ativada, ou vice-versa, ou quebrar dependendo do código circundante. A comparação de outros indicadores ainda fornecerá uma resposta, mas o idioma não define o significado dessa resposta (se houver). Não, é permitido bater. É verdadeiramente UB.
Peter Cordes
3
@ Adam: Oh sim, deixa pra lá a primeira parte do meu comentário, eu interpretei mal o seu. Mas você afirma que a comparação de outros indicadores ainda dará uma resposta . Isso não é verdade. Isso seria um resultado não especificado , não UB completo. UB é muito pior e significa que seu programa pode falhar ou SIGILL se a execução atingir essa instrução com essas entradas (a qualquer momento antes ou depois do que realmente acontece). (Somente plausível no x86-64 se o UB estiver visível no momento da compilação, mas geralmente tudo pode acontecer.) Parte do objetivo do UB é permitir que o compilador faça suposições "inseguras" ao gerar asm.
Peter Cordes

Respostas:

33

De acordo com a norma C11 , os operadores relacionais <, <=, >, e >=só pode ser usado em ponteiros para os elementos da mesma matriz ou objeto estrutura. Isso está detalhado na seção 6.5.8p5:

Quando dois ponteiros são comparados, o resultado depende dos locais relativos no espaço de endereço dos objetos apontados. Se dois ponteiros para os tipos de objeto apontam para o mesmo objeto, ou ambos apontam um após o último elemento do mesmo objeto de matriz, eles são comparados da mesma forma. Se os objetos apontados são membros do mesmo objeto agregado, ponteiros para membros da estrutura declarados posteriormente comparam mais que ponteiros com membros declarados anteriormente na estrutura e ponteiros para elementos de matriz com valores de subscrito maiores comparam maiores que ponteiros com elementos da mesma matriz com valores mais baixos de índice. Todos os ponteiros para membros do mesmo objeto de união são iguais.

Observe que todas as comparações que não atendem a esse requisito invocam um comportamento indefinido , o que significa (entre outras coisas) que você não pode depender da repetição dos resultados.

No seu caso particular, tanto para a comparação entre os endereços de duas variáveis ​​locais quanto entre o endereço de um local e um endereço dinâmico, a operação parecia "funcionar"; no entanto, o resultado pode mudar fazendo uma alteração aparentemente não relacionada ao seu código ou até compilar o mesmo código com diferentes configurações de otimização. Com comportamento indefinido, apenas porque o código pode falhar ou gerar um erro não significa que será .

Como exemplo, um processador x86 em execução no modo real 8086 possui um modelo de memória segmentada usando um segmento de 16 bits e um deslocamento de 16 bits para criar um endereço de 20 bits. Portanto, nesse caso, um endereço não converte exatamente em um número inteiro.

Os operadores de igualdade ==e, !=no entanto, não têm essa restrição. Eles podem ser usados ​​entre dois ponteiros para tipos compatíveis ou ponteiros NULL. Portanto, usar ==ou !=nos dois exemplos produziria código C válido.

No entanto, mesmo com ==e !=você pode obter resultados inesperados, mas ainda bem definidos. Consulte Uma comparação de igualdade de ponteiros não relacionados pode ser avaliada como verdadeira? para mais detalhes sobre isso.

Em relação à pergunta do exame feita pelo seu professor, ele faz várias suposições erradas:

  • Existe um modelo de memória plana onde existe uma correspondência 1 para 1 entre um endereço e um valor inteiro.
  • Que os valores do ponteiro convertido se ajustem dentro de um tipo inteiro.
  • Que a implementação simplesmente trate os ponteiros como números inteiros ao executar comparações sem explorar a liberdade dada pelo comportamento indefinido.
  • Que uma pilha é usada e que variáveis ​​locais são armazenadas lá.
  • Que um heap é usado para extrair a memória alocada.
  • Que a pilha (e, portanto, variáveis ​​locais) aparece em um endereço mais alto que o heap (e, portanto, objetos alocados).
  • As constantes da string aparecem em um endereço inferior ao da pilha.

Se você executasse esse código em uma arquitetura e / ou com um compilador que não atenda a essas suposições, poderá obter resultados muito diferentes.

Além disso, os dois exemplos também exibem comportamento indefinido quando chamam strcpy, já que o operando direito (em alguns casos) aponta para um único caractere e não para uma sequência terminada nula, resultando na leitura da função além dos limites da variável especificada.

dbush
fonte
3
@Shisui Mesmo assim, você ainda não deve depender dos resultados. Os compiladores podem ficar muito agressivos quando se trata de otimização e usarão comportamentos indefinidos como uma oportunidade para fazê-lo. É possível que o uso de um compilador diferente e / ou configurações diferentes de otimização possa gerar resultados diferentes.
dbush 29/12/19
2
@Shisui: Em geral, funcionará em máquinas com um modelo de memória plana, como x86-64. Alguns compiladores para esses sistemas podem até definir o comportamento em sua documentação. Mas, se não, o comportamento "insano" pode acontecer devido ao UB visível em tempo de compilação. (Na prática, eu não acho que ninguém quer que por isso não é compiladores algo principais procurar e "tentar quebrar".)
Peter Cordes
11
Como se um compilador visse que um caminho de execução levaria <entre mallocresultado e uma variável local (armazenamento automático, ou seja, pilha), ele poderia assumir que o caminho de execução nunca é usado e compilar toda a função em uma ud2instrução (gera um erro ilegal). - exceção de instrução com a qual o kernel lidará entregando um SIGILL ao processo). O GCC / clang faz isso na prática para outros tipos de UB, como cair no final de uma não voidfunção. godbolt.org está fora do ar agora, ao que parece, mas tente copiar / colar int foo(){int x=2;}e observe a falta de umret
Peter Cordes
4
@Shisui: TL: DR: não é C portátil, apesar de funcionar bem no Linux x86-64. Porém, fazer suposições sobre os resultados da comparação é uma loucura. Se você não estiver no encadeamento principal, sua pilha de encadeamentos será alocada dinamicamente usando o mesmo mecanismo mallocusado para obter mais memória do sistema operacional; portanto, não há razão para supor que seus vars locais (pilha de encadeamentos) estejam acima mallocda alocação dinâmica armazenamento.
Peter Cordes
2
@ PeterCordes: O que é necessário é reconhecer vários aspectos do comportamento como "opcionalmente definido", de modo que as implementações possam defini-los ou não, à vontade, mas devem indicar de maneira testável (por exemplo, macro predefinida) se não o fizerem. Além disso, em vez de caracterizar que qualquer situação em que os efeitos de uma otimização sejam observáveis ​​como "Comportamento indefinido", seria muito mais útil dizer que os otimizadores podem considerar certos aspectos do comportamento como "não observáveis" se indicarem que eles faça isso. Por exemplo, dado int x,y;, uma implementação ... #
297
12

O principal problema com a comparação de ponteiros com duas matrizes distintas do mesmo tipo é que as próprias matrizes não precisam ser colocadas em um determinado posicionamento relativo - uma pode acabar antes e depois da outra.

Primeiro de tudo, pensei que seria indefinido ou algum tipo ou erro, porque pt um px não está apontando para a mesma matriz (pelo menos no meu entendimento).

Não, o resultado depende da implementação e de outros fatores imprevisíveis.

Também é pt> ​​px porque os dois ponteiros estão apontando para as variáveis ​​armazenadas na pilha e a pilha cresce, portanto o endereço de memória de t é maior que o de x? É por isso que pt> px é verdadeiro?

Não há necessariamente uma pilha . Quando existe, não precisa crescer. Poderia crescer. Pode ser não-contíguo de alguma maneira bizarra.

Além disso, acho que a aritmética dos ponteiros entre dois ponteiros é boa, não importa para onde eles apontem individualmente, porque a aritmética está apenas usando os endereços de memória que os ponteiros armazenam.

Vejamos a especificação C , §6.5.8 na página 85, que discute operadores relacionais (ou seja, os operadores de comparação que você está usando). Observe que isso não se aplica ao direto !=ou à ==comparação.

Quando dois ponteiros são comparados, o resultado depende dos locais relativos no espaço de endereço dos objetos apontados. ... Se os objetos apontados são membros do mesmo objeto agregado, ... ponteiros para elementos da matriz com valores maiores de índice subscrito comparam maiores que ponteiros para elementos da mesma matriz com valores menores de índice subscrito.

Em todos os outros casos, o comportamento é indefinido.

A última frase é importante. Embora eu corte alguns casos não relacionados para economizar espaço, há um caso que é importante para nós: duas matrizes, que não fazem parte do mesmo objeto struct / agregado 1 , e estamos comparando ponteiros com essas duas matrizes. Esse é um comportamento indefinido .

Enquanto seu compilador acabou de inserir algum tipo de instrução de máquina CMP (comparação) que compara numericamente os ponteiros, e você teve sorte aqui, o UB é um animal muito perigoso. Literalmente, tudo pode acontecer - seu compilador pode otimizar toda a função, incluindo efeitos colaterais visíveis. Poderia gerar demônios nasais.

1 Ponteiros para duas matrizes diferentes que fazem parte da mesma estrutura podem ser comparados, pois isso se enquadra na cláusula em que as duas matrizes fazem parte do mesmo objeto agregado (a estrutura).

nanofarad
fonte
11
Mais importante, com te xsendo definido na mesma função, não há motivo para supor nada sobre como um compilador direcionado para x86-64 colocará os locais na estrutura da pilha para essa função. A pilha crescente para baixo não tem nada a ver com a ordem de declaração das variáveis ​​em uma função. Mesmo em funções separadas, se um pudesse se alinhar no outro, os locais da função "filho" ainda poderiam se misturar com os pais.
Peter Cordes
11
seu compilador poderia otimizar a toda a função, incluindo efeitos colaterais visíveis Não exagero: para outros tipos de UB (como cair no final de um não- voidfunção) g ++ e clang ++ realmente fazer isso na prática: godbolt.org/z/g5vesB eles suponha que o caminho da execução não seja utilizado porque leva ao UB e compile esses blocos básicos para uma instrução ilegal. Ou sem nenhuma instrução, apenas caindo silenciosamente para o que for mais próximo se essa função já foi chamada. (Por alguma razão gccnão faz isso, apenas g++).
Peter Cordes
6

Então perguntou o que

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Avalie para. A resposta é 0, 1 e 0.

Essas perguntas se reduzem a:

  1. É a pilha acima ou abaixo da pilha.
  2. É a pilha acima ou abaixo da seção literal de cadeia de caracteres do programa.
  3. mesmo que [1].

E a resposta para todos os três é "implementação definida". As perguntas do seu professor são falsas; eles o basearam no layout unix tradicional:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

mas vários órgãos modernos (e sistemas alternativos) não estão de acordo com essas tradições. A menos que prefaciem a questão com "a partir de 1992"; certifique-se de dar -1 na avaliação.

mevets
fonte
3
Não implementação definida, indefinida! Pense dessa maneira: o primeiro pode variar entre as implementações, mas as implementações devem documentar como o comportamento é decidido. O último significa que o comportamento pode variar de qualquer maneira, e a implementação não precisa lhe dizer agachamento :-) #
384
11
@paxdiablo: De acordo com a justificativa dos autores da Norma, "Comportamento indefinido ... também identifica áreas de possível extensão de linguagem em conformidade: o implementador pode aprimorar a linguagem fornecendo uma definição do comportamento oficialmente indefinido". A justificativa diz ainda: "O objetivo é dar ao programador uma chance de criar programas C poderosos que também são altamente portáteis, sem parecer menosprezar programas C perfeitamente úteis que, por acaso, não são portáteis, portanto, o advento estritamente". Os escritores de compiladores comerciais entendem isso, mas outros escritores de compiladores não.
Supercat
Há outro aspecto definido pela implementação; comparação de ponteiro é assinada , portanto, dependendo da máquina / os / compilador, alguns endereços podem ser interpretados como negativos. Por exemplo, uma máquina de 32 bits que colocou a pilha em 0xc << 28, provavelmente mostraria as variáveis ​​automáticas em um endereço menor que o heap ou rodata.
Mevets
11
@ mevets: A Norma especifica alguma situação em que a assinatura dos ponteiros nas comparações seja observável? Eu esperaria que, se uma plataforma de 16 bits permitir objetos maiores que 32768 bytes e arr[]esse objeto, o Padrão exigiria arr+32768uma comparação maior do que arrmesmo se uma comparação de ponteiro assinado reportasse o contrário.
Supercat
Eu não sei; o padrão C está orbitando no nono círculo de Dante, orando pela eutanásia. O OP referenciou especificamente K&R e uma pergunta do exame. #UB são detritos de um grupo de trabalho lento.
Mevets
1

Em quase qualquer plataforma remotamente moderna, ponteiros e números inteiros têm uma relação de ordem isomórfica, e ponteiros para objetos separados não são intercalados. A maioria dos compiladores expõe essa ordem aos programadores quando as otimizações estão desativadas, mas o Padrão não faz distinção entre plataformas que possuem essa ordem e aquelas que não têm e não exigem que nenhuma implementação exponha essa ordem ao programador, mesmo em plataformas que Defina isso. Conseqüentemente, alguns escritores de compilador executam vários tipos de otimizações e "otimizações" com base na suposição de que o código nunca comparará o uso de operadores relacionais em ponteiros para objetos diferentes.

De acordo com a justificativa publicada, os autores da norma pretendiam que as implementações estendessem a linguagem especificando como se comportariam em situações que a norma caracteriza como "comportamento indefinido" (ou seja, onde a norma não impõe requisitos ), ao fazê-lo, seria útil e prático , mas alguns autores de compiladores preferem assumir que os programas nunca tentarão se beneficiar de algo além do que o Padrão exige, do que permitir que os programas explorem de maneira útil comportamentos que as plataformas poderiam suportar sem nenhum custo extra.

Não conheço nenhum compilador projetado comercialmente que faça algo estranho com comparações de ponteiros, mas, à medida que os compiladores se deslocam para o LLVM não comercial para fins de back-end, é cada vez mais provável que processem códigos sem sentido, cujo comportamento foi especificado anteriormente compiladores para suas plataformas. Esse comportamento não se limita aos operadores relacionais, mas pode até afetar a igualdade / desigualdade. Por exemplo, embora o Padrão especifique que uma comparação entre um ponteiro para um objeto e um ponteiro "just past" para um objeto imediatamente anterior irá comparar iguais, os compiladores baseados em gcc e LLVM tendem a gerar código sem sentido, se os programas executarem tais comparações.

Como exemplo de uma situação em que até a comparação de igualdade se comporta de maneira absurda no gcc e no clang, considere:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

Tanto o clang quanto o gcc gerarão código que sempre retornará 4, mesmo que xsejam dez elementos, yo siga imediatamente e iseja zero, resultando na comparação verdadeira e p[0]sendo escrita com o valor 1. Acho que o que acontece é que uma passagem de otimização reescreve a função como se *p = 1;fosse substituída por x[10] = 1;. O último código seria equivalente se o compilador interpretasse *(x+10)como equivalente *(y+i), mas, infelizmente, um estágio de otimização a jusante reconhece que um acesso ao x[10]seria definido apenas se xtivesse pelo menos 11 elementos, o que tornaria impossível esse acesso y.

Se os compiladores puderem obter esse "criativo" com o cenário de igualdade de ponteiros descrito pelo Padrão, eu não confiaria que eles evitassem ser ainda mais criativos nos casos em que o Padrão não impõe requisitos.

supercat
fonte
0

É simples: comparar ponteiros não faz sentido, pois nunca é garantido que os locais de memória dos objetos estejam na mesma ordem em que você os declarou. A exceção são matrizes. & array [0] é menor que & array [1]. Isso é o que K&R aponta. Na prática, os endereços dos membros da estrutura também estão na ordem em que você os declara na minha experiência. Não há garantias disso .... Outra exceção é se você comparar um ponteiro para igual. Quando um ponteiro é igual a outro, você sabe que está apontando para o mesmo objeto. O que quer que seja. Pergunta do exame ruim, se você me perguntar. Dependendo do Ubuntu Linux 16.04, ambiente de programação da versão de 64 bits para uma pergunta do exame? Realmente ?

Hans Lepoeter
fonte
Tecnicamente, as matrizes não são realmente uma exceção desde que você não declarar arr[0], arr[1], etc separadamente. Como você declara arrcomo um todo, a ordem dos elementos individuais da matriz é um problema diferente do descrito nesta pergunta.
31419
11
É garantido que os elementos da estrutura estejam em ordem, o que garante que alguém possa usar memcpypara copiar uma parte contígua de uma estrutura e afetar todos os elementos nela contidos e não afetar mais nada. O Padrão é desleixado quanto à terminologia sobre que tipos de aritmética de ponteiro podem ser feitos com estruturas ou malloc()armazenamento alocado. A offsetofmacro seria bastante inútil se não fosse possível o mesmo tipo de ponteiro aritmético com os bytes de uma estrutura que com a char[], mas o Padrão não diz expressamente que os bytes de uma estrutura são (ou podem ser usados ​​como) um objeto de matriz.
Supercat
-4

Que pergunta provocativa!

Até a verificação superficial das respostas e comentários deste tópico revelará o quão emotiva sua consulta aparentemente simples e direta acaba sendo.

Não deveria ser surpreendente.

Indiscutivelmente, mal - entendidos sobre o conceito e o uso de ponteiros representam uma causa predominante de falhas graves na programação em geral.

O reconhecimento dessa realidade é prontamente evidente na onipresença de linguagens projetadas especificamente para abordar e, de preferência, para evitar os desafios que os indicadores apresentam por completo. Pense em C ++ e outros derivados de C, Java e suas relações, Python e outros scripts - apenas como os mais proeminentes e predominantes, e mais ou menos ordenados em severidade ao lidar com o problema.

O desenvolvimento de uma compreensão mais profunda dos princípios subjacentes deve, portanto, ser pertinente a todo indivíduo que aspira à excelência em programação - especialmente no nível de sistemas .

Imagino que seja exatamente isso que seu professor pretende demonstrar.

E a natureza de C o torna um veículo conveniente para esta exploração. Menos claramente que o assembly - embora talvez seja mais facilmente compreensível - e ainda muito mais explicitamente do que linguagens baseadas em abstrações mais profundas do ambiente de execução.

Projetado para facilitar a tradução determinística da intenção do programador em instruções que as máquinas possam compreender, C é uma linguagem no nível do sistema . Embora classificado como de alto nível, ele realmente pertence a uma categoria 'média'; mas como não existe, a designação de 'sistema' deve ser suficiente.

Essa característica é amplamente responsável por torná-la um idioma de escolha para drivers de dispositivo , código do sistema operacional e implementações incorporadas . Além disso, uma alternativa merecidamente favorecida em aplicações onde a eficiência ideal é fundamental; onde isso significa a diferença entre sobrevivência e extinção e, portanto, é uma necessidade em oposição a um luxo. Nesses casos, a atraente conveniência da portabilidade perde todo o seu fascínio, e optar pelo desempenho sem brilho do denominador menos comum se torna uma opção impensável e prejudicial .

O que torna C - e alguns de seus derivados - bastante especial é que ele permite que seus usuários tenham controle total - quando é o que desejam - sem impor as responsabilidades relacionadas a eles quando não o fazem. No entanto, nunca oferece mais do que o mais fino dos isolamentos da máquina , pelo que o uso adequado exige uma compreensão rigorosa do conceito de ponteiros .

Em essência, a resposta para sua pergunta é subliminarmente simples e satisfatoriamente doce - em confirmação de suas suspeitas. Desde que , no entanto, se atribua a importância necessária a todos os conceitos nesta declaração:

  • Os atos de examinar, comparar e manipular indicadores são sempre e necessariamente válidos, enquanto as conclusões derivadas do resultado dependem da validade dos valores contidos e, portanto, não precisam ser.

O primeiro é invariavelmente seguro e potencialmente adequado , enquanto o último só pode ser adequado quando tiver sido estabelecido como seguro . Surpreendentemente - para alguns - , o estabelecimento da validade do último depende e exige o primeiro.

Obviamente, parte da confusão surge do efeito da recursão inerentemente presente no princípio de um ponteiro - e dos desafios colocados na diferenciação de conteúdo de endereço.

Você supôs corretamente ,

Estou sendo levado a pensar que qualquer ponteiro pode ser comparado a qualquer outro ponteiro, independentemente de onde eles apontem individualmente. Além disso, acho que a aritmética dos ponteiros entre dois ponteiros é boa, não importa para onde eles apontem individualmente, porque a aritmética está apenas usando os endereços de memória que os ponteiros armazenam.

E vários colaboradores afirmaram: ponteiros são apenas números. Às vezes, algo mais próximo de números complexos , mas ainda não mais do que números.

A acrimônia divertida em que essa afirmação foi recebida aqui revela mais sobre a natureza humana do que sobre programação, mas permanece digna de nota e elaboração. Talvez o façamos mais tarde ...

Como um comentário começa a sugerir; toda essa confusão e consternação deriva da necessidade de discernir o que é válido e o que é seguro , mas isso é uma simplificação excessiva. Também devemos distinguir o que é funcional e o que é confiável , o que é prático e o que pode ser adequado e ainda mais: o que é apropriado em uma circunstância específica do que pode ser adequado em um sentido mais geral . Para não mencionar; a diferença entre conformidade e propriedade .

Para isso, primeiro precisamos apreciar precisamente o que um ponteiro é .

  • Você demonstrou uma firme aderência ao conceito e, como alguns outros, pode achar essas ilustrações simplistas, mas o nível de confusão evidente aqui exige tanta simplicidade no esclarecimento.

Como vários apontaram: o termo ponteiro é apenas um nome especial para o que é simplesmente um índice e, portanto, nada mais que qualquer outro número .

Isso já deve ser evidente por considerar o fato de que todos os computadores convencionais contemporâneos são máquinas binárias que necessariamente trabalham exclusivamente com números . A computação quântica pode mudar isso, mas isso é altamente improvável e não atingiu a maioridade.

Tecnicamente, como você observou, os ponteiros são endereços mais precisos ; um insight óbvio que naturalmente introduz a analogia gratificante de correlacioná-los com os "endereços" de casas ou lotes na rua.

  • Em um modelo de memória plana : toda a memória do sistema é organizada em uma única sequência linear: todas as casas da cidade ficam na mesma estrada e cada casa é identificada exclusivamente pelo seu número. Deliciosamente simples.

  • Em esquemas segmentados : uma organização hierárquica de estradas numeradas é introduzida acima da de casas numeradas, para que endereços compostos sejam necessários.

    • Algumas implementações ainda são mais complicadas, e a totalidade de 'estradas' distintas não precisa somar uma sequência contígua, mas nada disso muda nada sobre o subjacente.
    • Somos necessariamente capazes de decompor cada vínculo hierárquico em uma organização plana. Quanto mais complexa a organização, mais arcos precisaremos percorrer para fazê-lo, mas deve ser possível. De fato, isso também se aplica ao 'modo real' no x86.
    • Caso contrário, o mapeamento de links para locais não seria bijetivo , pois a execução confiável - no nível do sistema - exige que DEVE ser.
      • vários endereços não devem ser mapeados para locais de memória singulares e
      • endereços singulares nunca devem ser mapeados para vários locais de memória.

Trazendo-nos para uma nova reviravolta que transforma o enigma em um emaranhado tão fascinantemente complicado . Acima, foi conveniente sugerir que ponteiros são endereços, por uma questão de simplicidade e clareza. Claro, isso não está correto. Um ponteiro não é um endereço; um ponteiro é uma referência a um endereço , contém um endereço . Como o envelope ostenta uma referência à casa. Contemplar isso pode levar você a vislumbrar o que significava com a sugestão de recursão contida no conceito. Ainda; temos apenas tantas palavras, e falando sobre os endereços de referências a endereçose assim, logo interrompe a maioria dos cérebros com uma exceção inválida do código operacional . E, na maioria das vezes, a intenção é prontamente obtida do contexto, portanto, voltemos à rua.

Os trabalhadores postais nesta nossa cidade imaginária são muito parecidos com os que encontramos no mundo "real". É provável que ninguém sofra um derrame quando você falar ou perguntar sobre um endereço inválido , mas todos os últimos serão reprovados quando você solicitar que eles usem essas informações.

Suponha que haja apenas 20 casas em nossa rua singular. Finja ainda que uma alma disléxica ou equivocada direcionou uma carta, muito importante, para o número 71. Agora, podemos perguntar ao nosso transportador Frank, se existe um endereço assim, e ele simplesmente e calmamente informará: não . Podemos até mesmo esperar que ele estimar quão longe fora da rua este local iria mentir se fez existir: cerca de 2,5 vezes mais do que o fim. Nada disso lhe causará exasperação. No entanto, se pedíssemos a ele que entregasse esta carta ou pegasse um item daquele local, é provável que ele seja bastante franco com relação ao seu descontentamento e se recuse a cumpri-lo.

Ponteiros são apenas endereços e endereços são apenas números.

Verifique a saída do seguinte:

void foo( void *p ) {
   printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}

Ligue-o para quantos ponteiros você quiser, válido ou não. Por favor, não postar seus resultados se ele falhar em sua plataforma, ou o seu (contemporânea) compilador reclama.

Agora, como os ponteiros são simplesmente números, é inevitavelmente válido compará-los. Em certo sentido, é exatamente isso que seu professor está demonstrando. Todas as seguintes afirmações são perfeitamente válidas - e adequadas! - C, e quando compilado será executado sem encontrar problemas , mesmo que nenhum ponteiro precise ser inicializado e os valores que eles contêm, portanto, podem ser indefinidos :

  • Estamos apenas calculando result explicitamente por uma questão de clareza e imprimindo -o para forçar o compilador a calcular o que, de outra forma, seria um código morto redundante.
void foo( size_t *a, size_t *b ) {
   size_t result;
   result = (size_t)a;
   printf(“%zu\n”, result);
   result = a == b;
   printf(“%zu\n”, result);
   result = a < b;
   printf(“%zu\n”, result);
   result = a - b;
   printf(“%zu\n”, result);
}

Obviamente, o programa é mal formado quando a ou b é indefinido (leia-se: não foi inicializado corretamente ) no momento do teste, mas isso é totalmente irrelevante para esta parte da nossa discussão. Esses trechos, assim como as instruções a seguir, são garantidos - pelo 'padrão' - para compilar e executar sem falhas, apesar da validade de IN de qualquer ponteiro envolvido.

Os problemas só surgem quando um ponteiro inválido é desreferenciado . Quando pedimos a Frank para pegar ou entregar no endereço inválido e inexistente.

Dado qualquer ponteiro arbitrário:

int *p;

Enquanto esta declaração deve compilar e executar:

printf(“%p”, p);

... como deve:

size_t foo( int *p ) { return (size_t)p; }

... os dois seguintes, em contraste, ainda serão facilmente compilados, mas falharão na execução , a menos que o ponteiro seja válido - pelo que aqui queremos apenas dizer que ele faz referência a um endereço ao qual o presente aplicativo recebeu acesso :

printf(“%p”, *p);
size_t foo( int *p ) { return *p; }

Quão sutil é a mudança? A distinção está na diferença entre o valor do ponteiro - que é o endereço e o valor do conteúdo: da casa nesse número. Nenhum problema surge até que o ponteiro seja desreferenciado ; até que seja feita uma tentativa de acessar o endereço ao qual ele vincula. Ao tentar entregar ou pegar o pacote além do trecho da estrada ...

Por extensão, o mesmo princípio se aplica necessariamente a exemplos mais complexos, incluindo a necessidade acima mencionada de estabelecer a validade necessária:

int* validate( int *p, int *head, int *tail ) { 
    return p >= head && p <= tail ? p : NULL; 
}

A comparação relacional e a aritmética oferecem utilidade idêntica ao teste de equivalência e são equivalentemente válidas - em princípio. No entanto , o que os resultados de tal cálculo significariam é uma questão completamente diferente - e precisamente a questão abordada pelas citações que você incluiu.

Em C, uma matriz é um buffer contíguo, uma série linear ininterrupta de locais de memória. A comparação e a aritmética aplicadas aos ponteiros que referenciam locais dentro de uma série tão singular são naturalmente e obviamente significativas em relação uma à outra e a essa 'matriz' (que é simplesmente identificada pela base). O mesmo se aplica a todos os blocos alocados através de malloc, ou sbrk. Como esses relacionamentos estão implícitos , o compilador é capaz de estabelecer relacionamentos válidos entre eles e, portanto, pode ter certeza de que os cálculos fornecerão as respostas antecipadas.

A realização de ginástica semelhante em ponteiros que fazem referência a blocos ou matrizes distintos não oferece tal utilidade inerente e aparente . Além disso, uma vez que qualquer relação que exista em um momento pode ser invalidada por uma realocação a seguir, em que é altamente provável que isso mude, e até invertida. Nesses casos, o compilador não pode obter as informações necessárias para estabelecer a confiança que tinha na situação anterior.

Você , no entanto, como programador, pode ter esse conhecimento! E, em alguns casos, somos obrigados a explorar isso.

SÃO , portanto, as circunstâncias em que mesmo esta é totalmente válido e perfeitamente adequada.

De fato, é exatamente isso que mallocprecisa fazer internamente quando chega a hora de tentar mesclar blocos recuperados - na grande maioria das arquiteturas. O mesmo vale para o alocador de sistema operacional, como o anterior sbrk; se mais obviamente , freqüentemente , em entidades mais díspares , mais criticamente - e relevantes também em plataformas onde isso mallocpode não acontecer. E quantos deles não estão escritos em C?

A validade, segurança e sucesso de uma ação são inevitavelmente a conseqüência do nível de insight sobre o qual ela é premissa e aplicada.

Nas citações que você ofereceu, Kernighan e Ritchie estão abordando uma questão intimamente relacionada, mas ainda assim separada. Eles estão definindo as limitações do idioma e explicando como você pode explorar os recursos do compilador para protegê-lo, pelo menos, detectando construções potencialmente errôneas. Eles estão descrevendo os comprimentos que o mecanismo é capaz - foi projetado - para percorrer, a fim de ajudá-lo em sua tarefa de programação. O compilador é seu servo, você é o mestre. Um mestre sábio, porém, é aquele que está intimamente familiarizado com as capacidades de seus vários servos.

Nesse contexto, o comportamento indefinido serve para indicar perigo potencial e a possibilidade de dano; não implica desgraça iminente e irreversível, ou o fim do mundo como o conhecemos. Significa simplesmente que nós - "significando o compilador" - não somos capazes de fazer nenhuma conjetura sobre o que essa coisa pode ser ou representar e, por esse motivo, optamos por lavar as mãos do assunto. Não seremos responsabilizados por qualquer desventura que possa resultar do uso ou mau uso desta instalação .

Na verdade, ele simplesmente diz: 'Além deste ponto, cowboy : você está por sua conta ...'

Seu professor está tentando demonstrar as nuances mais refinadas para você.

Observe que grande cuidado eles tiveram ao elaborar seu exemplo; e como frágil que ainda é. Ao tomar o endereço de a, em

p[0].p0 = &a;

o compilador é forçado a alocar armazenamento real para a variável, em vez de colocá-lo em um registro. Sendo uma variável automática, no entanto, o programador não tem controle sobre onde isso é atribuído e, portanto, incapaz de fazer qualquer conjectura válida sobre o que o seguiria. É por isso que a deve ser definido como zero para que o código funcione conforme o esperado.

Apenas alterando esta linha:

char a = 0;

para isso:

char a = 1;  // or ANY other value than 0

faz com que o comportamento do programa fique indefinido . No mínimo, a primeira resposta agora será 1; mas o problema é muito mais sinistro.

Agora, o código está convidando para um desastre.

Embora ainda seja perfeitamente válido e até esteja em conformidade com o padrão , agora está mal formado e, apesar de compilado, pode falhar na execução por vários motivos. Por enquanto, existem vários problemas - nenhum dos quais o compilador é capaz de reconhecer.

strcpycomeçará no endereço de ae continuará além disso para consumir - e transferir - byte após byte, até encontrar um nulo.

O p1ponteiro foi inicializado em um bloco de exatamente 10 bytes.

  • Se aacontecer de ser colocado no final de um bloco e o processo não tiver acesso ao que se segue, a próxima leitura - de p0 [1] - provocará um segfault. Esse cenário é improvável na arquitetura x86, mas possível.

  • Se a área além do endereço de a estiver acessível, nenhum erro de leitura ocorrerá, mas o programa ainda não será salvo do infortúnio.

  • Se um byte zero, acontece a ocorrer dentro de dez iniciando no endereço de a, ele pode ainda sobreviver, para, em seguida, strcpyirá parar e, pelo menos, não vai sofrer uma escrita violação.

  • Se for não falha para leitura de errado, mas não zero bytes ocorre neste período de 10, strcpyvai continuar e tentar escrever para além do bloco alocado pelo malloc.

    • Se essa área não pertencer ao processo, o segfault deve ser acionado imediatamente.

    • O ainda mais desastroso - e sutil --- situação surge quando o bloco seguinte é de propriedade do processo, para, em seguida, o erro não pode ser detectado, nenhum sinal pode ser levantada, e por isso pode 'aparecer' ainda 'trabalho' , enquanto na verdade substituirá outros dados, as estruturas de gerenciamento do alocador ou mesmo o código (em certos ambientes operacionais).

É por isso que os erros relacionados ao ponteiro podem ser tão difíceis de rastrear . Imagine essas linhas enterradas profundamente em milhares de linhas de código intrinsecamente relacionadas, que outra pessoa escreveu, e você é instruído a se aprofundar.

No entanto , o programaainda deve ser compilado, pois permanece perfeitamente válido e em conformidade com o padrão C.

Esses tipos de erros, nenhum padrão e nenhum compilador podem proteger os incautos. Eu imagino que é exatamente isso que eles pretendem lhe ensinar.

As pessoas paranóicas procuram constantemente mudar a natureza de C para dispor dessas possibilidades problemáticas e, assim, nos salvar de nós mesmos; mas isso é falso . Essa é a responsabilidade que somos obrigados a aceitar quando escolhemos buscar o poder e obter a liberdade que o controle mais direto e abrangente da máquina nos oferece. Promotores e perseguidores da perfeição no desempenho nunca aceitarão nada menos.

A portabilidade e a generalidade que representa é uma consideração fundamentalmente separada e tudo o que o padrão procura abordar:

Este documento especifica a forma e estabelece a interpretação dos programas expressos na linguagem de programação C. Seu objetivo é promover a portabilidade , a confiabilidade, a manutenção e a execução eficiente de programas da linguagem C em uma variedade de sistemas de computação .

É por isso que é perfeitamente apropriado mantê-lo distinto da definição e especificação técnica da própria linguagem. Ao contrário do que muitos parecem acreditar que a generalidade é antitética ao excepcional e ao exemplar .

Concluir:

  • O exame e a manipulação de ponteiros são invariavelmente válidos e geralmente frutíferos . A interpretação dos resultados pode ou não ser significativa, mas a calamidade nunca é convidada até que o ponteiro seja desreferenciado ; até que seja feita uma tentativa de acessar o endereço vinculado.

Se isso não fosse verdade, a programação como a conhecemos - e a amamos - não teria sido possível.

Ghii Velte
fonte
3
Infelizmente, esta resposta é inerentemente inválida. Você não pode raciocinar nada sobre comportamento indefinido. A comparação não precisa ser feita no nível da máquina.
Antti Haapala
6
Ghii, na verdade não. Se você observar o Anexo J C11 e 6.5.8, o próprio ato de comparação é UB. A desreferenciação é uma questão separada.
precisa
6
Não, o UB ainda pode ser prejudicial antes mesmo que um ponteiro seja desreferenciado. Um compilador é livre para otimizar completamente uma função com o UB em um único NOP, mesmo que isso obviamente altere o comportamento visível.
Nanofarad
2
@ Ghii, o anexo J (a parte que mencionei) é a lista de coisas que são um comportamento indefinido , então não tenho certeza de como isso suporta seu argumento :-) 6.5.8 explicitamente chama a comparação como UB. Para o seu comentário ao supercat, não há comparação acontecendo quando você imprime um ponteiro; portanto, você provavelmente está certo de que ele não trava. Mas não era sobre isso que o OP estava perguntando. 3.4.3também é uma seção que você deve observar: define UB como comportamento "para o qual esta Norma Internacional não impõe requisitos".
precisa
3
@GhiiVelte, você continua afirmando coisas que estão claramente erradas, apesar de isso ter sido apontado para você. Sim, o snippet que você postou deve ser compilado, mas sua afirmação de que é executado sem problemas está incorreta. Sugiro que você realmente leia o padrão, particularmente (neste caso) C11 6.5.6/9, tendo em mente que a palavra "deve" indica um requisitoL "Quando dois ponteiros são subtraídos, ambos apontam para elementos do mesmo objeto de matriz ou um após o último elemento do objeto de matriz ".
precisa
-5

Ponteiros são apenas números inteiros, como tudo o mais em um computador. É absolutamente possível compará-los com <e >e produzir resultados sem causar um programa para falhar. Dito isto, o padrão não garante que esses resultados tenham algum significado fora das comparações de array.

No seu exemplo de variáveis ​​alocadas à pilha, o compilador é livre para alocá-las a registradores ou empilhar endereços de memória, e na ordem que desejar. Comparações como <e, >portanto, não serão consistentes entre compiladores ou arquiteturas. No entanto, ==e !=não são tão restritos, comparar a igualdade de ponteiros é uma operação válida e útil.

nickelpro
fonte
2
A pilha de palavras aparece exatamente zero vezes no padrão C11. E comportamento indefinido significa que tudo pode acontecer (incluindo falha no programa).
precisa
11
@paxdiablo Eu disse que sim?
Nickelpro 29/12/19
2
Você mencionou variáveis ​​alocadas à pilha. Não há pilha no padrão, isso é apenas um detalhe de implementação. O problema mais sério com esta resposta é a afirmação de que você pode comparar ponteiros sem chance de travamento - isso é errado.
precisa
11
@nickelpro: Se alguém deseja escrever um código que seja compatível com os otimizadores no gcc e no clang, é necessário pular muitos obstáculos. Ambos os otimizadores buscarão agressivamente oportunidades para extrair inferências sobre o que as coisas serão acessadas pelos ponteiros sempre que houver alguma maneira de distorcer o Padrão para justificá-las (e mesmo às vezes quando não houver). Dado que int x[10],y[10],*p;, se o código avalia y[0], avalia p>(x+5)e grava *psem modificar pnesse ínterim e, finalmente, avalia y[0]novamente, ... #
287
11
nickelpro, concorde em discordar, mas sua resposta ainda está fundamentalmente errada. Eu comparo sua abordagem com a das pessoas que usam, em (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')vez de isalpha()porque qual implementação sã teria esses caracteres descontínuos? O ponto principal é que, mesmo que nenhuma implementação que você conhece tenha um problema, você deve codificar o padrão o máximo possível, se valorizar a portabilidade. Eu aprecio o rótulo "standards maven", obrigado por isso. Eu posso colocar no meu CV :-) #
2100