Deve-se verificar todos os pequenos erros em C?


60

Como um bom programador, deve-se escrever códigos robustos que lidem com todos os resultados do seu programa. No entanto, quase todas as funções da biblioteca C retornarão 0 ou -1 ou NULL quando houver um erro.

Às vezes, é óbvio que a verificação de erros é necessária, por exemplo, quando você tenta abrir um arquivo. Mas muitas vezes eu ignoro a verificação de erros em funções como printfou mesmo mallocporque não me sinto necessário.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

É uma boa prática ignorar apenas certos erros ou existe uma maneira melhor de lidar com todos os erros?


13
Depende do nível de robustez necessário para o projeto em que você está trabalhando. Os sistemas que têm a chance de receber informações de terceiros não confiáveis ​​(por exemplo, servidores públicos) ou operar em ambientes não totalmente confiáveis, precisam ser codificados com muito cuidado, para evitar que o código se torne uma bomba-relógio (ou o link mais fraco sendo invadido) ) Obviamente, projetos de hobby e aprendizado não precisam de tanta robustez.
rwong

11
Alguns idiomas fornecem exceções. Se você não capturar exceções, seu encadeamento será encerrado, o que é melhor do que deixá-lo continuar com mau estado. Se você escolher capturar exceções, poderá cobrir muitos erros em várias linhas de código, incluindo funções e métodos invocados com uma tryinstrução, para que você não precise verificar todas as chamadas ou operações. (Observe também que alguns idiomas são melhores que outros na detecção de erros simples, como desreferenciação nula ou índice de matriz fora dos limites.)
Erik Eidt

11
O problema é principalmente metodológico: você não escreve a verificação de erros normalmente quando ainda está descobrindo o que deve implementar e não deseja adicionar a verificação de erros agora porque deseja obter o "caminho feliz" certo primeiro. Mas você ainda deve procurar por malloc and co. Em vez de pular a etapa de verificação de erros, você deve priorizar suas atividades de codificação e permitir que a verificação de erros seja uma etapa implícita de refatoração permanente em sua lista TODO, aplicada sempre que você estiver satisfeito com seu código. Adicionar comentários "/ * CHECK * /" greppable pode ajudar.
Coredump #

7
Não acredito que ninguém foi mencionado errno! Caso você não esteja familiarizado, embora seja verdade que "quase todas as funções da biblioteca C retornarão 0 ou -1 ou NULLquando houver um erro", elas também definem a errnovariável global , que você pode acessar usando #include <errno.h>e simplesmente lendo o valor de errno. Portanto, por exemplo, se open(2) retornar -1, convém verificar se errno == EACCES, o que indicaria um erro de permissão ou o ENOENTque indicaria que o arquivo solicitado não existe.
Wchargin

11
O @ErikEidt C não suporta try/ catch, embora você possa simulá-lo com saltos.
Mastro

Respostas:


29

Em geral, o código deve lidar com condições excepcionais sempre que apropriado. Sim, esta é uma afirmação vaga.

Em linguagens de nível superior, com manipulação de exceção de software, isso geralmente é declarado como "captura a exceção no método em que você realmente pode fazer algo a respeito". Se ocorreu um erro no arquivo, talvez você permita que a pilha chegue ao código da interface do usuário que pode realmente dizer ao usuário "seu arquivo não foi salvo no disco". O mecanismo de exceção engole efetivamente "todo pequeno erro" e o envolve implicitamente no local apropriado.

Em C, você não tem esse luxo. Existem algumas maneiras de lidar com erros, alguns dos quais são recursos de linguagem / biblioteca, outros são práticas de codificação.

É uma boa prática ignorar apenas certos erros ou existe uma maneira melhor de lidar com todos os erros?

Ignorar certos erros? Talvez. Por exemplo, é razoável supor que a gravação na saída padrão não falhará. Se falhar, como você diria ao usuário, afinal? Sim, é uma boa ideia ignorar certos erros ou codificar defensivamente para evitá-los. Por exemplo, verifique zero antes de dividir.

Existem maneiras de lidar com todos ou pelo menos a maioria dos erros:

  1. Você pode usar saltos, semelhantes aos gotos, para tratamento de erros . Embora seja uma questão controversa entre os profissionais de software, há usos válidos para eles, especialmente em códigos incorporados e de desempenho crítico (por exemplo, kernel do Linux).
  1. Cascading ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
    
  2. Teste a primeira condição, por exemplo, abrir ou criar um arquivo e, em seguida, supor que as operações subsequentes tenham êxito.

Código robusto é bom e deve-se verificar e manipular erros. Qual método é o melhor para o seu código depende do que o código faz, do quão crítica é uma falha etc., e somente você pode realmente responder a isso. No entanto, esses métodos são testados em batalha e usados ​​em vários projetos de código aberto, onde você pode ver como o código real verifica se há erros.


21
Desculpe, um pequeno detalhe para escolher aqui - "a gravação na saída padrão não falhará. Se falhar, como você diria ao usuário, afinal?" - escrevendo para erro padrão? Não há garantia de que uma falha na gravação de um implique que é impossível gravar no outro.
15-13:

4
@Damien_The_Unbeliever - especialmente porque stdoutpode ser redirecionado para um arquivo ou até não existir, dependendo do sistema. stdoutnão é um fluxo "write to console", geralmente é isso, mas não precisa ser.
Davor Ždralo

3
@JAB Você envia uma mensagem para stdout... óbvio :-)
TripeHound

7
@JAB: Você pode sair com EX_IOERR ou mais, se for apropriado.
John Marshall

6
Faça o que fizer, não continue correndo como se nada tivesse acontecido! Se o comando dump_database > backups/db_dump.txtfalhar na gravação da saída padrão em um determinado momento, eu não gostaria que ele continuasse e saísse com êxito. (Não que os bancos de dados são copiados dessa maneira, mas o ponto ainda está de pé)
Ben S

15

No entanto, quase todas as funções da biblioteca C retornarão 0 ou -1 ou NULL quando houver um erro.

Sim, mas você sabe para qual função chamou, não sabe?

Você realmente tem muitas informações que pode colocar em uma mensagem de erro. Você sabe qual função foi chamada, o nome da função que a chamou, quais parâmetros foram passados ​​e o valor de retorno da função. São muitas informações para uma mensagem de erro muito informativa.

Você não precisa fazer isso para todas as chamadas de função. Mas a primeira vez que você vir a mensagem de erro "Ocorreu um erro ao exibir o erro anterior", quando o que você realmente precisava era de informações úteis, será a última vez que você verá essa mensagem de erro, porque você mudará imediatamente a mensagem de erro para algo informativo que o ajudará a solucionar o problema.


5
Essa resposta me fez sorrir, porque é verdade, mas não responde à pergunta.
precisa

Como isso não responde à pergunta?
Robert Harvey

2
Uma razão pela qual eu acho que C (e outras línguas) misturou o booleano 1 e 0 é o mesmo motivo que qualquer função decente retornando um código de erro retorna "0" sem nenhum erro. mas então você tem que dizer if (!myfunc()) {/*proceed*/}. 0 não foi um erro e qualquer coisa diferente de zero foi um tipo de erro e cada tipo de erro teve seu próprio código diferente de zero. da maneira que um amigo meu disse: "existe apenas uma verdade, mas muitas falsidades". "0" deve ser "verdadeiro" no if()teste e qualquer coisa diferente de zero deve ser "falso".
Robert Bristow-johnson

11
não, fazendo do seu jeito, existe apenas um código de erro negativo possível. e muitos códigos possíveis no_error.
Robert Bristow-johnson

11
@ robertbristow-johnson: Em seguida, leia-o como "Não houve erro". if(function()) { // do something }lê como "se a função for executada sem erro, faça alguma coisa". ou, melhor ainda, "se a função for executada com êxito, faça alguma coisa". Sério, quem entende a convenção não se confunde com isso. Retornar false(ou 0) quando ocorrer um erro não permitiria o uso de códigos de erro. Zero significa falso em C, porque é assim que a matemática funciona. Veja programmers.stackexchange.com/q/198284
Robert Harvey

11

TLDR; você quase nunca deve ignorar erros.

A linguagem C não possui um bom recurso de manipulação de erros, deixando para cada desenvolvedor de biblioteca implementar suas próprias soluções. Os idiomas mais modernos têm exceções, o que facilita muito o manuseio desse problema em particular.

Mas quando você está preso ao C, não tem essas vantagens. Infelizmente, você simplesmente terá que pagar o preço toda vez que chamar uma função, com a possibilidade remota de falha. Caso contrário, você sofrerá consequências muito piores, como sobrescrever dados na memória sem querer. Portanto, como regra geral, você sempre deve verificar se há erros.

Se você não verificar o retorno, fprintfé muito provável que você deixe um bug para trás que, na melhor das hipóteses, não fará o que o usuário espera e, na pior das hipóteses, explodirá a coisa toda durante o vôo. Não há desculpa para se prejudicar dessa maneira.

No entanto, como desenvolvedor C, também é seu trabalho facilitar a manutenção do código. Portanto, às vezes, você pode ignorar com segurança os erros por uma questão de clareza se (e somente se) eles não representarem nenhuma ameaça ao comportamento geral do aplicativo. .

É o mesmo problema que fazer isso:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

Se você perceber que sem bons comentários dentro do bloco de captura vazio, esse é certamente um problema. Não há motivo para pensar que uma chamada malloc()seja executada com sucesso 100% do tempo.


Concordo que a manutenção é um aspecto realmente importante e esse parágrafo realmente respondeu à minha pergunta. Obrigado pela resposta detalhada.
Derek朕會功夫

Eu acho que os bilhões de programas que nunca se preocupam em verificar suas chamadas malloc e ainda assim nunca travam são um bom motivo para serem preguiçosos quando um programa de desktop usa apenas alguns MBs de memória.
whatsisname 17/11/2015

5
@whatsisname: Só porque eles não travam não significa que eles não estão corrompendo dados silenciosamente. malloc()é uma função em que você definitivamente deseja verificar os valores de retorno!
TMN

11
@TMN: Se o malloc falhasse, o programa seria imediatamente falhado e terminado, ele não continuaria fazendo as coisas. É tudo sobre risco e o que é apropriado, não "quase nunca".
Whatsisname

2
O Alex C não possui um bom tratamento de erros. Se você quiser exceções, poderá usá-las. A biblioteca padrão que não usa exceções é uma escolha deliberada (as exceções obrigam você a ter um gerenciamento automatizado de memória e, geralmente, você não deseja isso para código de baixo nível).
11789 martinshelev

9

A questão não é realmente específica do idioma, mas específica do usuário. Pense na pergunta da perspectiva do usuário. O usuário faz algo, como digitar o nome do programa em uma linha de comando e pressionar Enter. O que o usuário espera? Como eles podem dizer se algo deu errado? Eles podem se permitir interceder se ocorrer um erro?

Em muitos tipos de código, essas verificações são um exagero. No entanto, em códigos críticos de segurança de alta confiabilidade, como os de reatores nucleares, a verificação de erros patológicos e os caminhos de recuperação planejados fazem parte da natureza cotidiana do trabalho. Considera-se vale o custo gastar um tempo para perguntar "O que acontece se o X falhar? Como faço para voltar a um estado seguro?" Em códigos menos confiáveis, como os de videogame, você pode obter muito menos verificação de erros.

Outra abordagem semelhante é quanto você pode realmente melhorar o estado capturando o erro? Não posso contar o número de programas em C ++ que orgulhosamente capturam exceções, apenas para repassá-los porque eles realmente não sabiam o que fazer com eles ... mas eles sabiam que deveriam lidar com exceções. Tais programas não ganharam nada com o esforço extra. Adicione apenas o código de verificação de erros que você acha que pode realmente lidar com a situação melhor do que simplesmente não verificar o código de erro. Dito isto, o C ++ possui regras específicas para lidar com exceções que ocorrem durante o tratamento de exceções, a fim de capturá-las de uma maneira mais significativa (e com isso quero dizer chamar terminate () para garantir que a pira funerária que você construiu para si acenda em sua própria glória)

Quão patológico você pode ficar? Eu trabalhei em um programa que definia um "SafetyBool" cujos valores verdadeiros e falsos eram cuidadosamente escolhidos para ter uma distribuição uniforme de 1 e 0 e foram escolhidos para que qualquer uma das várias falhas de hardware (inversão de bit único, barramento de dados rastreamentos quebrados etc.) não fizeram com que um booleano fosse mal interpretado. Desnecessário dizer que eu não afirmaria que essa é uma prática de programação de uso geral a ser usada em qualquer programa antigo.


6
  • Diferentes requisitos de segurança exigem diferentes níveis de correção. No software de controle de aviação ou automóvel, todos os valores de retorno serão verificados, cf. MISRA.FUNC.UNUSEDRET . Em uma prova rápida de conceito que nunca sai de sua máquina, provavelmente não.
  • Codificação custa tempo. Muitas verificações irrelevantes em softwares que não são críticos para a segurança são um esforço melhor gasto em outros lugares. Mas onde está o ponto ideal entre qualidade e custo? Isso depende das ferramentas de depuração e da complexidade do software.
  • O tratamento de erros pode obscurecer o fluxo de controle e introduzir novos erros. Eu gosto bastante das funções de wrapper de Richard "network" Stevens, que pelo menos relatam erros.
  • O tratamento de erros pode, raramente, ser um problema de desempenho. Mas a maioria das chamadas da biblioteca C leva tanto tempo que o custo de verificar um valor de retorno é imensamente pequeno.

3

Um pouco de uma visão abstrata sobre a questão. E não é necessariamente para a linguagem C.

Para programas maiores, você teria uma camada de abstração; talvez uma parte do mecanismo, uma biblioteca ou dentro de uma estrutura. Essa camada não se preocupam com o tempo você começa a dados válidos ou a saída seria algum valor padrão: 0, -1, nulletc.

Depois, há uma camada que seria sua interface para a camada abstrata, que faria muito tratamento de erros e talvez outras coisas como injeções de dependência, escuta de eventos etc.

E mais tarde, você teria sua camada de implementação concreta, na qual você realmente define as regras e lida com a saída.

Portanto, minha opinião é que às vezes é melhor excluir completamente o tratamento de erros de uma parte do código, porque essa parte simplesmente não faz esse trabalho. E, em seguida, tenha algum processador que avalie a saída e aponte para um erro.

Isso é feito principalmente para separar responsabilidades que levam à legibilidade do código e a uma melhor escalabilidade.


0

Em geral, a menos que você tenha um bom motivo para não verificar essa condição de erro, deve. A única boa razão pela qual posso pensar para não verificar uma condição de erro é quando você não pode fazer algo significativo se ela falhar. Esta é uma barra muito difícil de encontrar, porque sempre há uma opção viável: sair normalmente.


Em geral, eu discordo de você, pois erros são situações que você não considerou. É difícil saber como o erro pode se manifestar se você não souber sob que condição o erro ocorreu. E, portanto, tratamento de erros antes do processamento real dos dados.
Creative Magic

Como, eu disse, se algo retornar ou gerar um erro, mesmo se você não souber como corrigi-lo e continuar, sempre poderá falhar normalmente, registrar o erro, informar ao usuário que não funcionou conforme o esperado, etc. O ponto é que o erro não deve passar despercebido, desmarcado, "falha silenciosa" ou travar o aplicativo. Isso é o que significa "falhar normalmente" ou "sair normalmente".
Clever Neologism
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.