Resumo :
Uma função em C sempre deve verificar se não está referenciando um NULL
ponteiro? Caso contrário, quando é apropriado ignorar essas verificações?
Detalhes :
Eu tenho lido alguns livros sobre entrevistas de programação e estou me perguntando qual é o grau apropriado de validação de entrada para argumentos de função em C? Obviamente, qualquer função que receba informações de um usuário precisa executar a validação, incluindo a verificação de um NULL
ponteiro antes de desferenciá-lo. Mas e no caso de uma função no mesmo arquivo que você não espera expor por meio de sua API?
Por exemplo, o seguinte aparece no código-fonte do git:
static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
if (!want_color(graph->revs->diffopt.use_color))
return column_colors_max;
return graph->default_column_color;
}
Se *graph
for NULL
, um ponteiro nulo será desreferenciado, provavelmente travando o programa, mas possivelmente resultando em outro comportamento imprevisível. Por outro lado, a função é static
e, portanto, talvez o programador já tenha validado a entrada. Eu não sei, eu apenas o selecionei aleatoriamente, porque foi um pequeno exemplo em um programa aplicativo escrito em C. Eu já vi muitos outros lugares em que ponteiros são usados sem verificar NULL. Minha pergunta é geral, não específica para este segmento de código.
Vi uma pergunta semelhante dentro do contexto de entrega de exceções . No entanto, para uma linguagem não segura como C ou C ++, não há propagação automática de erros de exceções não tratadas.
Por outro lado, vi muitos códigos em projetos de código aberto (como o exemplo acima) que não fazem nenhuma verificação de ponteiros antes de usá-los. Eu estou querendo saber se alguém tem idéias sobre diretrizes para quando colocar cheques em uma função vs. assumindo que a função foi chamada com argumentos corretos.
Estou interessado nesta questão em geral para escrever código de produção. Mas também estou interessado no contexto das entrevistas de programação. Por exemplo, muitos livros didáticos de algoritmos (como CLR) tendem a apresentar os algoritmos no pseudocódigo sem nenhuma verificação de erro. No entanto, embora isso seja bom para entender o núcleo de um algoritmo, obviamente não é uma boa prática de programação. Portanto, não gostaria de dizer a um entrevistador que estava ignorando a verificação de erros para simplificar meus exemplos de código (como um livro didático). Mas eu também não gostaria de parecer produzir código ineficiente com verificação de erro excessiva. Por exemplo, o graph_get_current_column_color
poderia ter sido modificado para verificar se *graph
há nulo, mas não está claro o que faria se *graph
fosse nulo, além de não desreferê-lo.
fonte
Respostas:
Ponteiros nulos inválidos podem ser causados por erro do programador ou por erro de tempo de execução. Erros de tempo de execução são algo que um programador não pode consertar, como uma
malloc
falha devido à falta de memória ou a rede descartando um pacote ou o usuário digitando algo estúpido. Os erros do programador são causados por um programador que utiliza a função incorretamente.A regra geral que eu vi é que os erros de tempo de execução sempre devem ser verificados, mas os erros do programador não precisam ser verificados todas as vezes. Digamos que algum programador idiota chame diretamente
graph_get_current_column_color(0)
. Ele irá falhar na primeira vez em que for chamado, mas depois que você o corrigir, o reparo será compilado permanentemente. Não é necessário verificar todas as vezes que é executado.Às vezes, especialmente em bibliotecas de terceiros, você verá um
assert
para verificar os erros do programador em vez de umaif
instrução. Isso permite que você compile as verificações durante o desenvolvimento e as deixe de fora no código de produção. Ocasionalmente, também vi verificações gratuitas em que a origem do erro potencial do programador está muito longe do sintoma.Obviamente, você sempre pode encontrar alguém mais pedante, mas a maioria dos programadores em C que conheço favorece código menos confuso do que o marginalmente mais seguro. E "mais seguro" é um termo subjetivo. Um segfault flagrante durante o desenvolvimento é preferível a um erro sutil de corrupção no campo.
fonte
Kernighan & Plauger, em "Ferramentas de Software", escreveu que eles verificariam tudo e, para condições que acreditavam que nunca poderiam acontecer, abortariam com a mensagem de erro "Não pode acontecer".
Eles relatam ser humilhados rapidamente pelo número de vezes que viram "Não pode acontecer" sair em seus terminais.
SEMPRE verifique o NULL do ponteiro antes de (tentar) desreferê-lo. SEMPRE . A quantidade de código que você duplica verificando NULLs que não acontecem e o processador faz o "desperdício" será mais do que paga pelo número de falhas que você não precisa depurar de nada além de um despejo de memória - se você tiver essa sorte.
Se o ponteiro é invariante dentro de um loop, basta checá-lo fora do loop, mas você deve "copiá-lo" em uma variável local com escopo limitado, para uso pelo loop, que adiciona as decorações const apropriadas. Nesse caso, você DEVE garantir que todas as funções chamadas do corpo do loop incluam as decorações const necessárias nos protótipos, TODO O CAMINHO. Se não o fizer, ou não pode (por causa do ex pacote de um fornecedor ou um colega de trabalho obstinado), então você deve verificar-lo para NULL cada vez que poderia ser modificado , porque certo como COL Murphy era um otimista incurável, alguém ESTÁ acontecendo zapá-lo quando você não está olhando.
Se você estiver dentro de uma função e o ponteiro não for NULL chegando, verifique-o.
Se você o estiver recebendo de uma função, e não for NULL, deve verificar. malloc () é particularmente notório por isso. (A Nortel Networks, agora extinta, tinha um padrão de codificação por escrito muito rígido sobre isso. Eu depurei uma falha em um ponto, que retornei ao malloc () retornando um ponteiro NULL e o codificador idiota não se incomodando em verificar antes de escrever, porque ele sabia que tinha muita memória ... Eu disse algumas coisas muito desagradáveis quando finalmente a encontrei.)
fonte
assert
, com certeza. Não gosto da ideia do código de erro se você estiver falando sobre alterar o código existente para incluirNULL
verificações.Você pode pular a verificação quando se convencer de que o ponteiro não pode ser nulo.
Normalmente, as verificações de ponteiro nulo são implementadas no código em que se espera que nulo apareça como um indicador de que um objeto não está disponível no momento. Nulo é usado como um valor sentinela, por exemplo, para encerrar listas vinculadas ou mesmo matrizes de ponteiros. É necessário que o
argv
vetor de seqüências transmitidasmain
seja finalizado por nulo por um ponteiro, da mesma forma que uma sequência é terminada por um caractere nulo:argv[argc]
é um ponteiro nulo, e você pode confiar nisso ao analisar a linha de comando.Portanto, as situações para verificação de nulo são aquelas em que é um valor esperado. As verificações nulas implementam o significado do ponteiro nulo, como interromper a pesquisa de uma lista vinculada. Eles impedem que o código desreferencie o ponteiro.
Em uma situação em que um valor de ponteiro nulo não é esperado por design, não há sentido em procurá-lo. Se um valor de ponteiro inválido surgir, provavelmente parecerá não nulo, o que não pode ser diferenciado dos valores válidos de nenhuma maneira portátil. Por exemplo, um valor de ponteiro obtido da leitura de armazenamento não inicializado interpretado como um tipo de ponteiro, um ponteiro obtido por meio de alguma conversão sombria ou um ponteiro incrementado fora dos limites.
Sobre um tipo de dados como
graph *
: this pode ser projetado para que um valor nulo seja um gráfico válido: algo sem arestas e sem nós. Nesse caso, todas as funções que usam umgraph *
ponteiro terão que lidar com esse valor, pois é um valor de domínio correto na representação de gráficos. Por outro lado, agraph *
poderia ser um ponteiro para um objeto semelhante a um contêiner que nunca é nulo se mantivermos um gráfico; um ponteiro nulo pode nos dizer que "o objeto gráfico não está presente; ainda não o alocamos ou o liberamos; ou que atualmente não possui um gráfico associado". Esse último uso de ponteiros é um booleano / satélite combinado: o ponteiro sendo não nulo indica "Eu tenho esse objeto irmão" e fornece esse objeto.Podemos definir um ponteiro para null, mesmo se não estivermos liberando um objeto, simplesmente para dissociar um objeto do outro:
fonte
Deixe-me acrescentar mais uma voz à fuga.
Como muitas das outras respostas, eu digo - não se preocupe em checar neste momento; é responsabilidade do interlocutor. Mas tenho uma base para basear-me em vez de uma simples conveniência (e arrogância de programação em C).
Tento seguir o princípio de Donald Knuth de tornar os programas o mais frágil possível. Se algo der errado, faça com que ele caia muito e fazer referência a um ponteiro nulo geralmente é uma boa maneira de fazer isso. A ideia geral é que uma falha ou um loop infinito é muito melhor do que criar dados errados. E chama a atenção dos programadores!
Porém, fazer referência a ponteiros nulos (especialmente para grandes estruturas de dados) nem sempre causa uma falha. Suspiro. Isso é verdade. E é aí que o Asserts se enquadra. Eles são simples, podem travar seu programa instantaneamente (o que responde à pergunta "O que o método deve fazer se encontrar um nulo?") E pode ser ativado / desativado para várias situações (eu recomendo NÃO os desative, pois é melhor que os clientes tenham uma falha e vejam uma mensagem enigmática do que dados incorretos).
Esses são meus dois centavos.
fonte
Geralmente, só checo quando um ponteiro é atribuído, que geralmente é o único momento em que posso realmente fazer algo a respeito e possivelmente recuperar se for inválido.
Se eu conseguir um identificador para uma janela, por exemplo, vou verificar se ele está nulo certo e depois e ali e fazer algo sobre a condição nula, mas não vou verificar se ele está nulo todas as vezes Eu uso o ponteiro, em todas as funções que o ponteiro é passado, caso contrário, eu teria montanhas de código de manipulação de erro duplicado.
Funções como
graph_get_current_column_color
é provavelmente completamente incapaz de fazer algo útil à sua situação se encontrar um ponteiro ruim, portanto, deixaria a verificação de NULL para seus chamadores.fonte
Eu diria que depende do seguinte:
A utilização da CPU / Odds Pointer é NULL Toda vez que você verifica NULL, leva tempo. Por esse motivo, tento limitar meus cheques aonde o ponteiro poderia ter seu valor alterado.
Sistema Preemptivo Se o seu código estiver em execução e outra tarefa puder interrompê-lo e, potencialmente, alterar o valor que seria bom ter uma verificação.
Módulos fortemente acoplados Se o sistema estiver fortemente acoplado, faria sentido que você tenha mais verificações. O que quero dizer com isso é que, se houver estruturas de dados que são compartilhadas entre vários módulos, um módulo pode mudar algo de outro módulo. Nessas situações, faz sentido verificar com mais frequência.
Verificações automáticas / assistência de hardware A última coisa a considerar é se o hardware em que você está executando possui algum tipo de mecanismo que pode verificar se há NULL. Refiro-me especificamente à detecção de falhas de página. Se o seu sistema possui detecção de falha de página, a própria CPU pode verificar acessos NULL. Pessoalmente, acho que esse é o melhor mecanismo, pois sempre é executado e não depende do programador para fazer verificações explícitas. Ele também tem o benefício de praticamente zero de sobrecarga. Se estiver disponível, eu recomendo, a depuração é um pouco mais difícil, mas não excessivamente.
Para testar se está disponível, crie um programa com um ponteiro. Defina o ponteiro como 0 e tente lê-lo / gravá-lo.
fonte
Na minha opinião, validar entradas (pré / pós-condições, ie) é uma boa coisa para detectar erros de programação, mas apenas se resultar em erros altos e desagradáveis, que mostram o tipo de interrupção, que não podem ser ignorados.
assert
normalmente tem esse efeito.Qualquer coisa que não chegue a esse ponto pode se transformar em um pesadelo sem equipes cuidadosamente coordenadas. E, é claro, o ideal é que todas as equipes sejam cuidadosamente coordenadas e unificadas sob padrões rígidos, mas a maioria dos ambientes em que trabalhei ficou muito aquém disso.
Apenas como exemplo, eu trabalhei com alguns colegas que acreditavam que se deveria religiosamente verificar a presença de ponteiros nulos, para que eles espalhassem muitos códigos como este:
... e às vezes assim mesmo sem retornar / definir um código de erro. E isso ocorreu em uma base de código com várias décadas de existência e muitos plugins de terceiros adquiridos. Também era uma base de código atormentada por muitos bugs, e freqüentemente eram muito difíceis de rastrear até as causas principais, pois tinham uma tendência a travar em sites muito distantes da fonte imediata do problema.
E essa prática foi uma das razões. É uma violação de uma pré-condição estabelecida da
move_vertex
função acima passar um vértice nulo a ela, mas essa função simplesmente a aceitou silenciosamente e não fez nada em resposta. Então, o que tendia a acontecer era que um plug-in poderia ter um erro de programador que faz com que ele passe nulo à referida função, apenas para não detectá-lo, apenas para fazer muitas coisas depois e, eventualmente, o sistema começaria a desbotar ou travar.Mas o verdadeiro problema aqui foi a incapacidade de detectar facilmente esse problema. Então, uma vez tentei ver o que aconteceria se eu transformasse o código analógico acima em um
assert
, assim:... e, para meu horror, descobri que a afirmação falhou esquerda e direita, mesmo ao iniciar o aplicativo. Depois de consertar os primeiros sites de chamadas, fiz mais algumas coisas e, em seguida, recebi um barco com mais falhas de asserção. Continuei até ter modificado tanto código que acabei revertendo minhas alterações porque elas se tornaram muito intrusivas e mantiveram relutantemente essa verificação de ponteiro nulo, em vez de documentar que a função permite aceitar um vértice nulo.
Mas esse é o perigo, ainda que no pior cenário, de deixar de detectar facilmente violações de pré / pós-condições. Você pode, ao longo dos anos, acumular silenciosamente um monte de códigos que violam essas condições pré / pós enquanto voam sob o radar dos testes. Na minha opinião, tais verificações de ponteiro nulo fora de uma falha flagrante e desagradável de afirmação podem realmente causar muito, muito mais mal do que bem.
Quanto à questão essencial de quando você deve procurar indicadores nulos, acredito em afirmar liberalmente se ele foi projetado para detectar um erro do programador, e não deixar que isso fique silencioso e difícil de detectar. Se não for um erro de programação e algo fora do controle do programador, como uma falta de memória, faz sentido verificar se há nulo e usar o tratamento de erros. Além disso, é uma questão de design e com base no que suas funções consideram condições pré / pós válidas.
fonte
Uma prática é sempre executar a verificação nula, a menos que você já a tenha verificado; portanto, se a entrada está sendo passada da função A () para B () e A () já validou o ponteiro e você tem certeza de que B () não é chamado em nenhum outro lugar, então B () pode confiar em que A () tenha higienizou os dados.
fonte
NULL
verificações extras sejam suficientes . Pense nisso: agoraB()
procuraNULL
e ... faz o que? Retorno-1
? Se o chamador não procurarNULL
, que confiança você tem de que ele vai lidar com o-1
caso do valor de retorno?