O uso de uma linguagem mais restrita não apenas move as postagens de meta, de obter a implementação correta para a especificação correta. É difícil fazer algo que está muito errado, mas consistente logicamente; é por isso que os compiladores capturam tantos bugs.
A aritmética dos ponteiros, como é normalmente formulada, é doentia porque o sistema de tipos não significa realmente o que deveria significar. Você pode evitar esse problema completamente trabalhando em um idioma de coleta de lixo (a abordagem normal que também faz você pagar pela abstração). Ou você pode ser muito mais específico sobre os tipos de ponteiros que está usando, para que o compilador possa rejeitar qualquer coisa que seja inconsistente ou que não consiga se provar correta como está escrita. Essa é a abordagem de alguns idiomas como o Rust.
Tipos construídos são equivalentes a provas; portanto, se você escrever um sistema de tipos que esquece isso, todos os tipos de coisas darão errado. Suponha por um tempo que, quando declaramos um tipo, na verdade queremos dizer que estamos afirmando a verdade sobre o que está na variável.
- int * x; // Uma afirmação falsa. x existe e não aponta para um int
- int * y = z; // Verdadeiro apenas se for comprovado que z aponta para um int
- * (x + 3) = 5; // Verdadeiro apenas se (x + 3) apontar para um int na mesma matriz que x
- int c = a / b; // Verdadeiro apenas se b for diferente de zero, como: "diferente de zero int b = ...;"
- nulo int * z = NULL; // nulo int * não é o mesmo que um int *
- int d = * z; // Uma afirmação falsa, porque z é anulável
- if (z! = NULL) {int * e = z; } // Ok, porque z não é nulo
- livre (y); int w = * y; // Declaração falsa, porque y não existe mais em w
Neste mundo, ponteiros não podem ser nulos. As desreferências NullPointer não existem e os ponteiros não precisam ser verificados quanto à nulidade em nenhum lugar. Em vez disso, um "int nulo *" é um tipo diferente que pode ter seu valor extraído para nulo ou para um ponteiro. Isso significa que, no ponto em que a suposição não nula é iniciada, você registra sua exceção ou desativa uma ramificação nula.
Nesse mundo, também não existem erros de matriz fora dos limites. Se o compilador não puder provar que está dentro dos limites, tente reescrever para que o compilador possa provar isso. Se não puder, você terá que inserir manualmente a Assunção nesse local; o compilador pode encontrar uma contradição mais tarde.
Além disso, se você não puder ter um ponteiro que não seja inicializado, não terá ponteiros para a memória não inicializada. Se você tiver um ponteiro para liberar memória, ele deverá ser rejeitado pelo compilador. No Rust, existem diferentes tipos de ponteiros para tornar razoável esse tipo de prova. Existem indicadores de propriedade exclusiva (ou seja, sem aliases), indicadores de estruturas profundamente imutáveis. O tipo de armazenamento padrão é imutável etc.
Há também o problema de impor uma gramática bem definida nos protocolos (que inclui membros da interface), para limitar a área da superfície de entrada exatamente ao que é esperado. A questão da "correção" é: 1) Livre-se de todos os estados indefinidos 2) Garanta consistência lógica . A dificuldade de chegar lá tem muito a ver com o uso de ferramentas extremamente ruins (do ponto de vista da correção).
É exatamente por isso que as duas piores práticas são variáveis globais e gotos. Essas coisas evitam colocar condições pré / pós / invariantes em torno de qualquer coisa. É também por isso que os tipos são tão eficazes. À medida que os tipos se fortalecem (finalmente, usando Tipos Dependentes para levar em consideração o valor real), eles se aproximam de serem provas construtivas de correção; programas inconsistentes falham na compilação.
Lembre-se de que não se trata apenas de erros estúpidos. Também se trata de defender a base de código de infiltradores inteligentes. Haverá casos em que você precisará rejeitar um envio sem uma prova convincente gerada por máquina de propriedades importantes como "segue o protocolo formalmente especificado".