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
p
eq
ponto 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 pt
e px
não estão apontando para a mesma matriz (pelo menos no meu entendimento).
Também é pt > px
porque 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
sbrk
podem ser comparados significativamente. Isso não é garantido pelo padrão, que permite comparações de ponteiros apenas dentro de uma matriz. Portanto, essa versãomalloc
é 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 1
a 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:
p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1
A resposta é 0
, 1
e 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 x86-64 , linux e talvez montagem . Se o ponto da pergunta / classe fosse especificamente detalhes de implementação de SO de baixo nível, em vez de C. portátil)
C
com o que é seguro emC
. 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).Respostas:
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: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:
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.fonte
<
entremalloc
resultado 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 umaud2
instruçã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ãovoid
função. godbolt.org está fora do ar agora, ao que parece, mas tente copiar / colarint foo(){int x=2;}
e observe a falta de umret
malloc
usado para obter mais memória do sistema operacional; portanto, não há razão para supor que seus vars locais (pilha de encadeamentos) estejam acimamalloc
da alocação dinâmica armazenamento.int x,y;
, uma implementação ... #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.
Não, o resultado depende da implementação e de outros fatores imprevisíveis.
Não há necessariamente uma pilha . Quando existe, não precisa crescer. Poderia crescer. Pode ser não-contíguo de alguma maneira bizarra.
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.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).
fonte
t
ex
sendo 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.void
funçã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ãogcc
não faz isso, apenasg++
).Essas perguntas se reduzem a:
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:
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.
fonte
arr[]
esse objeto, o Padrão exigiriaarr+32768
uma comparação maior do quearr
mesmo se uma comparação de ponteiro assinado reportasse o contrário.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:
Tanto o clang quanto o gcc gerarão código que sempre retornará 4, mesmo que
x
sejam dez elementos,y
o siga imediatamente ei
seja zero, resultando na comparação verdadeira ep[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 porx[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 aox[10]
seria definido apenas sex
tivesse pelo menos 11 elementos, o que tornaria impossível esse acessoy
.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.
fonte
É 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 ?
fonte
arr[0]
,arr[1]
, etc separadamente. Como você declaraarr
como um todo, a ordem dos elementos individuais da matriz é um problema diferente do descrito nesta pergunta.memcpy
para 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 oumalloc()
armazenamento alocado. Aoffsetof
macro 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 achar[]
, 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.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:
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 ,
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 é .
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.
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:
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 :
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.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:
Enquanto esta declaração deve compilar e executar:
... como deve:
... 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 :
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:
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
, ousbrk
. 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.
Há SÃO , portanto, as circunstâncias em que mesmo esta é totalmente válido e perfeitamente adequada.
De fato, é exatamente isso que
malloc
precisa 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 anteriorsbrk
; se mais obviamente , freqüentemente , em entidades mais díspares , mais criticamente - e relevantes também em plataformas onde issomalloc
pode 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
, emo 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:
para isso:
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.
strcpy
começará no endereço dea
e continuará além disso para consumir - e transferir - byte após byte, até encontrar um nulo.O
p1
ponteiro foi inicializado em um bloco de exatamente 10 bytes.Se
a
acontecer 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,strcpy
irá 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,
strcpy
vai continuar e tentar escrever para além do bloco alocado pelomalloc
.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:
É 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:
Se isso não fosse verdade, a programação como a conhecemos - e a amamos - não teria sido possível.
fonte
3.4.3
também é uma seção que você deve observar: define UB como comportamento "para o qual esta Norma Internacional não impõe requisitos".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 ".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.fonte
int x[10],y[10],*p;
, se o código avaliay[0]
, avaliap>(x+5)
e grava*p
sem modificarp
nesse ínterim e, finalmente, avaliay[0]
novamente, ... #(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')
vez deisalpha()
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 :-) #