sexta-feira, 13 de julho de 2012

Porque C não é a minha linguagem de programação favorita



Para Niklaus Wirth, a linguagem C foi rebocada pelo projeto UNIX. Grande parte do seu sucesso foi impulsionado pelo espirito da  “cultura hacker” e do software livre. Ela permitia que os programadores tivessem um controle maior dos recursos da máquina. Contudo, a liberdade proposta por ela deixar passar uma grande quantidade de erros de programação e ressuscitava a antiga paixão por códigos obscuros e truques astutos. Ideia bastante combatida por Djikstra:

“Duas opiniões sobre programação datam dessa época. Vou mencioná-las agora e retornarei a elas mais tarde. A primeira era a de que um programador realmente competente deveria ter destreza enigmática e gostar muito de truques inteligentes; a outra sustentava que programar nada mais era que otimizar a eficiência do processo computacional em uma ou outra direção.” 

Infelizmente, esses truques representam armadilhas que tornam grandes sistemas propensos a erros e um convite para códigos obscuros.

Dessa forma, o programador disciplinado proposto por Djikstra comprometido com uma metodologia formal não combinava com a cultura propagada pela linguagem C.  A seguir, vamos apresentar alguns problemas da linguagem C.

Falta de ortogonalidade
Em linguagem de programação, ortogonalidade significa que um conjunto relativamente pequeno de construções primitivas podem ser combinadas em um número pequeno de maneiras para construir as estruturas de controle e de dados de uma linguagem.

Na prática, uma linguagem ortogonal seria mais fácil de aprender e teria menos exceções. Cada exceção é uma construção que precisa ser aprendida para ser evitada.
Exemplos de falta de ortogonalidade em C:

  • Um struct podem ser valores de retorno de uma função, mas vetores não.
  • Um vetor pode ser valor de retorno se estiver encapsulado por um struct
  • Um membro de um struct pode ser qualquer tipo de dado exceto void ou um struct do mesmo tipo.
  • Um vetor pode ser de qualquer tipo exceto void.
  • Todos os tipos de dados são passados por valor exceto vetores
Por outro lado, o excesso de ortogonalidade é prejudicial:
  • Todas as instruções ( incluindo atribuições, if, while, etc) retornam valores e podem ser usado em expressões.
  • Operações lógicas e aritméticas podem aparecer misturadas.
Quem nunca sofreu com um erro desse tipo :  if (a=b+2) atire a primeira pedra!

Sem tipo booleano
$ cat > test.c
int main(void)
{
bool b;
return 0;
}

$ gcc -ansi -pedantic -Wall -W test.c
test.c: In function 'main':
test.c:3: 'bool' undeclared (first use in this function)


Até a padronização ISO C 1999, o tipo de dado booleano não tinha sido implementado. No ISO C 1999, ele é implementado como um macro e precisa incluir um cabeçalho para ser utilizado. O uso de tipos booleanos aumenta a legibilidade.



O tipo cadeira de caracteres não é primitivo

Na linguagem C, o tipo de cadeia de caracteres é um tipo especial de vetor de caracteres. As operações com cadeias de caracteres são realizadas por meio de uma biblioteca-padrão cujo arquivo de cabeçalho é string.h.

Na linguagem Python, as operações de concatenação, comparação e substrings são operações primitivas.
>>> a = "cadeia de"
>>> b = "caracteres"
>>> a+b
'cadeia decaracteres'
>>> a = "cadeia "
>>> b = "caracteres"
>>> a+b
'cadeia caracteres'
>>> a>b
False
>>> a[1:3]
'ad'



A falta de um tipo primitivo de cadeia de caracteres é um convite para o risco de buffer overflow (estouro de buffer) uma vez que você precisa saber de antemão quantos caracteres serão utilizados na sua cadeia de caracteres. Uma outra alternativa é trabalhar com alocação dinâmica usando malloc. Logo, você precisa ficar atento para os diversos problemas que podem ocorrer com os ponteiros:

  • Você não pode desalocar o mesmo ponteiro duas vezes.
  • Você precisa desalocar o ponteiro.
  • Você precisa verificar se a função malloc retornou uma região de memória válida.
  • Você não saber dizer se o ponteiro já foi liberado ou ainda está em uso.


Funções que incentivam buffer overflows

  • gets()
  • strcat()
  • strcpy()
  • sprintf()
  • vsprintf()
  • bcopy()
  • scanf()
  • fscanf()
  • sscanf()
  • getwd()
  • getopt()
  • realpath()
  • getpass()




Integer overflow sem aviso prévio
Auto-explicativo. Um minuto você tem um número de 15 dígitos, em seguida, tentar dobrar ou triplicar isso e boom! seu valor é de repente -234891234890892 ou algo similar.
>>> a = 2
>>> type(a)
>>> a = a + 12414214214213124214124214214214321
>>> type(a)

Na linguagem Python, não ocorre o estouro do inteiro porque a linguagem converte implicitamente para o tipo long.

Manipulação de erro inconsistentes

Um programador C experiente será capaz de dizer o que eu estou falando só de ler o título desta seção. Há muitas maneiras incompatíveis em que uma função de biblioteca C indica que ocorreu um erro:

  • Voltando zero. 
  • Voltando diferente de zero. 
  • Retornando um ponteiro NULL. 
  • Definir errno. 
  • Exige uma chamada para outra função. 
  • Produz uma mensagem de diagnóstico para o utilizador.


Ponteiros

Com os ponteiros, podemos passar horas e horas divertindo-se tentando encontrar vários tipos de erros diferentes.

Ponteiros pendurado
#include
#include
int main(){
 int *p1,*p2;
 p1 = (int*)malloc(sizeof(int));
 *p1 = 3;
 p2 = p1;
 free(p1);
 printf("%d\n",*p2);
 
}


O primeiro endereço alocado para p1 não é mais acessível ao programa.

Esse problema pode gerar o fenômeno conhecido como vazamento de memória (memory leak). Um simples erro pode consumir a memória completamente.
void f(void){
    void* s;
    s = malloc(50); /* obtém a memória */
    return;  /* memory leak - ver nota abaixo */ 
 
    /* 
     * A [[Memória (computador)|memória]] é obtida e disponibilizada em s.
     * Após o retorno da [[subrotina|função]], 
     *o [[Ponteiro (programação)|ponteiro]] é destruído e
     * perde-se a referência a esta porção de memória.
     * 
     * Este código pode ser consertado se a função f() incluir
     * a operação "free(s)" antes do retorno ou se o código
     * que chama f() fizer a chamada para a operação free().
     */
}
 
int main(void){
    /* Este ''loop'' infinito chama a função f(), definida acima */
    while (1) f(); /* Cedo ou tarde a chamada à função irá falhar, devido à um
                      um erro de alocação na função malloc, quando a memória 
                  terminar.*/
    return 0;
}



Os ponteiros têm sido comparados com a instrução goto que amplia a faixa de instruções a serem executadas em seguida. Os variáveis ponteiros ampliam a faixa de célula da memória que podem ser referenciadas por uma variável. A afirmação mais caluniosa sobre os ponteiros foi feita por Hoare (1973): “A introdução dos ponteiros nas linguagens de alto-nível foi um passo para trás, do qual talvez jamais possamos nos recuperar”.

Verificação da faixa dos subscritos

A linguagem C não verifica se os subscritos ou índices de um vetor estão dentro da faixa de valores definidas. A verificação da faixa é um fator importante na confiabilidade de uma linguagem.
>>> a = [1,2,3,4]
>>> a[3]
4
>>> a[4]

Traceback (most recent call last):
  File "", line 1, in
    a[4]
IndexError: list index out of range
>>>
Tipificação Fraca As conversões de tipo podem ser de estreitamento ou de alargamento. Uma conversão de estreitamento transforma um valor de um tipo para um outro tipo que não pode armazenar nem mesmo uma aproximação de todos os valores do tipo original. Exemplo: double para float, float para int. Uma conversão por alargamento muda um valor de um tipo para um outro que pode incluir, pelo menos, aproximações dos valores originais: Exemplo float para Double, int para float. As conversões por alagarmento são quase sempre seguras. Coerção é uma conversão implícita iniciada pelo computador. As coerções resulta na perda da tipificação forte. Por exemplo, Java tem a metade dos tipos de coerções em atribuições que o C++. As conversões de estreitamento precisam ser feitas explicitamente. Considere o seguinte exemplo em Python:
>>> a = 'a' + 2
Traceback (most recent call last):
  File "", line 1, in
    a = 'a' + 2
TypeError: cannot concatenate 'str' and 'int' objects
>>> 

Esse tipo de expressão seria realizada tranquilamente pela linguagem C.

Esses e outros problemas da linguagem C podem ser vistos aqui:
Why C Is Not My Favourite Programming Language
http://www.kuro5hin.org/story/2004/2/7/144019/8872/

Outros textos:
Brian W. Kernighan, April 2, 1981

Why Pascal is Not My Favorite Programming Language

http://www.lysator.liu.se/c/bwk-on-pascal.html





7 comentários:

Eduardo disse...

"O tipo cadeira de caracteres não é primitivo"

Creio que deveria ser cadeia.

COISAS DO FUTURO disse...

A LINGUAGEM C É COMO SE FOSSE UMA ESTRADA INFINITA. VOCÊ PODE CONSTRUIR PRATICAMENTE QUALQUER PROGRAMA. OBIVIAMENTE COMO QUALQUER ESTRADA SE EU SAIR FORA DELA EU NÃO CHEGO A LUGAR NENHUM. OS PONTEIROS PERMITEM DIVERSAS MANOBRAS NAS SOLUÇÕES DE PROBLEMAS COMPUTACIONAIS, CABE AO PROGRAMADOR ESCOLHER UMA SAIDA VIÁVEL PARA NÃO SE PERDER NAS DIVERSAS OPÇÕES QUE OS PONTEIROS DISPÔE.

mcpinto disse...

É sempre bom indicar as fontes e a autoria dos textos...

Claudio Roberto França Pereira disse...

C é diretamente traduzivel para ASM, e esse era o objetivo. Comparar Python com C é como comparar um Boeing com o 14 Bis.

Pedro disse...

Verdade. Ainda bem que alguém sabe disso ainda.

Sem esquecer que ser portável era outro dos maiores objetivos.

O problema maior é gente querendo usar C hoje em dia pra fazer coisas que são melhores (muito melhroes) programadas em outras linguagens.

No contexto certo, esses problemas de C que o autor apontou são na verdade soluções e conveniências. C é uma linguagem de uso prático (tem linguagem que é feita por acadêmicos, sem pensar muito no uso prático) que foi feita principalmente pra re-escrever boa parte do UNIX. Muitas coisas foram colocadas lá na linguagem pra que ela fosse conveniênte pra essa tarefa.

Por exemplo, se você evitar os milhões de situações indefinidas de C, é bem provável que o seu programa seja altamente portável (o que é não é bem verdade pra várias outras linguagens). O que era desejado na época, porque o povo queria escrever uma boa parte do UNIX só uma vez e ter essa parte funcionando em todo lugar. Você junta isso com o fato de que é "fácil" escrever um compilador simples de C pra Assembly, o resultado foi o que deu na época: UNIX em todo lugar.

Wladimir Araújo Tavares disse...

O objetivo desse texto foi gerar discussões sobre a linguagem C. Eu acredito que o texto atingiu o seu objetivo.

Notícia interessante sobre o C:
http://meiobit.com/107326/no-o-sistema-operacional-da-curiosity-no-linux-mas-voc-provavelmente-j-o-usou/

Carlson Santana Cruz disse...

*Um membro de um struct pode ser qualquer tipo de dado exceto void ou um struct do mesmo tipo. - Não vejo necessidade de um tipo de dado void sendo declarado, sobre um struct do mesmo tipo é uma pena, mas será que ele permite um ponteiro do mesmo tipo?
*Um vetor pode ser de qualquer tipo exceto void. - Não vejo necessidade nenhuma de um vetor do tipo void.
*Todos os tipos de dados são passados por valor exceto vetores. - Isso acontece porque se tinha a intenção de economizar processamento, e copiar um array é um processo que poderia ser demorado.
*O primeiro endereço alocado para p1 não é mais acessível ao programa. - Esse problema de perda de referência pode ocorrer na maioria das linguagens, a diferença é que algumas linguagens possuem um coletor de lixo para resolver isso.

Todo o resto concordo com você, mas uma coisa boa em C é a liberdade que o programador tem que nas linguagens mordenas foram tiradas de forma que não é possível realizar certas operações, além delas serem absurdamente mais lentas e gastarem mais memória que C. E C++ está aí com alguns erro sanados e outros novos (haha). Quem sabe D seja a salvação.