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.
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 anterior sbrk
; se mais obviamente , freqüentemente , em entidades mais díspares , mais criticamente - e relevantes também em plataformas onde isso malloc
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
, 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.
strcpy
começará no endereço de a
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 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.
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).