Quando os ponteiros devem ser verificados para NULL em C?


18

Resumo :

Uma função em C sempre deve verificar se não está referenciando um NULLponteiro? 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 NULLponteiro 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 *graphfor NULL, um ponteiro nulo será desreferenciado, provavelmente travando o programa, mas possivelmente resultando em outro comportamento imprevisível. Por outro lado, a função é statice, 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_colorpoderia ter sido modificado para verificar se *graphhá nulo, mas não está claro o que faria se *graphfosse nulo, além de não desreferê-lo.


7
Se você está escrevendo uma função para uma API em que os chamadores não devem entender as entranhas, esse é um daqueles lugares onde a documentação é importante. Se você documentar que um argumento deve ser um ponteiro válido e não NULL, a verificação se torna responsabilidade do chamador.
Blrfl


Com a retrospectiva do ano de 2017, tendo em mente a pergunta e a maioria das respostas foram escritas em 2013, alguma das respostas aborda a questão dos comportamentos indefinidos de viajar no tempo devido à otimização dos compiladores?
Rwong 15/01/19

No caso de chamadas de API que esperam argumentos válidos de ponteiro, pergunto-me qual é o valor do teste apenas para NULL? Qualquer ponteiro inválido que seja desreferenciado seria tão ruim quanto NULL e segfault da mesma forma.
PaulHK

Respostas:


15

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 mallocfalha 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 assertpara verificar os erros do programador em vez de uma ifinstruçã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.


A pergunta é um pouco subjetiva, mas esta parecia ser a melhor resposta por enquanto. Obrigado a todos que deram suas opiniões sobre esta questão.
Gabriel Southern

1
No iOS, o malloc nunca retornará NULL. Se ele não encontrar memória, solicitará que seu aplicativo libere memória primeiro, depois solicite ao sistema operacional (que solicitará que outros aplicativos liberem memória e possivelmente matem-nos) e, se ainda não houver memória, ele matará seu aplicativo . Não são necessárias verificações.
gnasher729

11

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.)


8
Se você estiver em uma função que requer um ponteiro que não seja NULL, mas verifique assim mesmo e é NULL ... o que vem a seguir?
detly

1
@detly quer parar o que está fazendo e retornar um código de erro, ou tropeçar uma declaração
James

1
@ James - não pensei assert, com certeza. Não gosto da ideia do código de erro se você estiver falando sobre alterar o código existente para incluir NULLverificações.
detly

10
@detly você não vai chegar muito longe como um dev C, se você não gosta códigos de erro
James

5
@ JohnR.Strohm - este é C, é asserções ou nada: P
detly

5

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 argvvetor de seqüências transmitidas mainseja 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.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

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 um graph *ponteiro terão que lidar com esse valor, pois é um valor de domínio correto na representação de gráficos. Por outro lado, a graph *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:

tty_driver->tty = NULL; /* detach low level driver from the tty device */

O argumento mais convincente que sei que um ponteiro não pode ser nulo em um determinado ponto é agrupar esse ponto em "if (ptr! = NULL) {" e um correspondente "}". Além disso, você está no território formal de verificação.
John R. Strohm

4

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.


1

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.


1

Eu diria que depende do seguinte:

  1. A utilização da CPU é crítica? Cada verificação de NULL leva algum tempo.
  2. Quais são as chances de o ponteiro ser NULL? Foi usado apenas em uma função anterior. O valor do ponteiro pode ter sido alterado.
  3. O sistema é preventivo? O que significa uma mudança de tarefa acontecer e alterar o valor? Um ISR poderia entrar e alterar o valor?
  4. Qual a rigidez do código?
  5. Existe algum tipo de mecanismo automático que verificará automaticamente ponteiros NULL?

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.


Não sei se classificaria um segfault como executando uma verificação NULL automática. Concordo que ter uma proteção de memória da CPU ajuda a evitar que um processo cause tanto dano ao restante do sistema, mas eu não chamaria isso de proteção automática.
Gabriel Southern

1

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. assertnormalmente 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:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... 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_vertexfunçã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:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... 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.


0

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.


1
... até daqui a 6 meses alguém aparece e adiciona mais um código que chama B () (possivelmente assumindo que quem escreveu B () certamente verificou os NULLs corretamente). Então você está ferrado, não é? Regra básica - se existir uma condição inválida para a entrada de uma função, verifique-a, porque a entrada está fora do controle da função.
Maximus Minimus

@ mh01 Se você está apenas quebrando código aleatório (ou seja, fazendo suposições e não lendo a documentação), não acho que NULLverificações extras sejam suficientes . Pense nisso: agora B()procura NULLe ... faz o que? Retorno -1? Se o chamador não procurar NULL, que confiança você tem de que ele vai lidar com o -1caso do valor de retorno?
detly

1
Essa é a responsabilidade dos chamadores. Você lida com sua própria responsabilidade, o que inclui não confiar em nenhuma informação arbitrária / desconhecida / potencialmente não verificada. Caso contrário, você está na cidade policial. Se o chamador não verificar, ele estragou tudo; você verificou, seu próprio traseiro está coberto, você pode dizer a quem escreveu que ligou que pelo menos você fez as coisas corretamente.
Maximus Minimus
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.