Logo Hardware.com.br
Henry-Keys
Henry-Keys Geek Registrado
1.8K Mensagens 235 Curtidas

5 vícios que devem largar em C

#1 Por Henry-Keys 09/05/2013 - 15:19
Imagem
5 vícios que devem largar na linguagem C


Introdução



Esse artigo tem como objetivo educar certos programadores evitarem técnicas ridículas e perigosas na linguagem C. Eu considero essas técnicas de vícios, pois até em universidades que conheço, existem trolhas (ops! professores) que ensinam "o mau caminho" aos futuros programadores profissionais.

Se este artigo for de tamanho interesse para si, peço a sua total atenção, pois acredito que vai ajudar a evitar e a superar certos problemas que alguns programadores encontram na linguagem C.

Vício 1 - Usar system("pause")

Bom, isso até o meu próprio professor de programação ensina mostrando_lingua.png. Como bem sabemos, o protótipo da função system() está declarada em stdlib.h, esta mesma função nos permite invocar comandos do sistema operativo em uso dentro de um código C/C++.

No Linux o comando clear por exemplo limpa a tela do terminal:
[code=rich]
system("clear");
[/code]
Já no Windows, usa-se o comando cls:
[code=rich]
system("cls");
[/code]

Mas, vamos nos focar no cerne deste tópico, o famigerado system("pause"). O pause é um comando interno do interpretador de comandos do Windows (aka cmd.exe), e tem o propósito de pausar ou interromper a janela de comandos (a tela preta) e emitir a mensagem "Prima qualquer tecla para continuar".

Muitos iniciantes na linguagem C usam essa aberração com o propósito de ver a saída ou o resultado de seus programas, antes da consola fechar e o programar terminar.
[code=rich]
#include

/* Este programa faz a soma entre 2 números e guarda o resultado em a*/
int a=2;
int b=2;

int main()
{
a = a+b;
printf("%i",a);
}

[/code]

Para a infelicidade de alguns, o código acima não dá a possibilidade do programador ver o resultado da soma na tela, "a tela pisca" e nem dá chance de eu ver os resultados. Foi daí que algum programador maluco, inventou a solução avassaladora que está no código abaixo:
[code=rich]
#include

/* Este programa faz a soma entre 2 números e guarda o resultado em a*/
int a=2;
int b=2;

int main()
{
a = a+b;
printf("%i",a);
system("pause"); //Mataram
}

[/code]

Até agora não expliquei o motivo desta técnica ser tão horrível, mas cá vai, primeiro é pelo simples fato de ser deselegante, pode não parecer, mas com o tempo irás perceber que sim.
O segundo motivo é a portabilidade, a linguagem C é muito elogiada em livros por sua portabilidade, ou seja, o código que você compilar e rodar em um processador ou sistema operativo provavelmente poderá ser compilado e rodado em outro.

Mas existem excessões, existem bibliotecas e funções que são desenhadas para um só sistema operativo ou processador e o uso das mesmas pode limitar o código de certo programa à um só sistema operativo.
O system("pause"), torna seu código não-portável pelo simples fato de ter o comando pause que é específico para sistemas operativos Windows, ou seja, essa invocação não irá funcionar no Linux, MacOS, FreeBSD, NetBSD, etc.

Porém, existe uma alternativa, usar a função getchar() que é portável pois faz parte do padrão ANSI C, observe o código abaixo:
[code=rich]
#include

/* Este programa faz a soma entre 2 números e guarda o resultado em a*/
int a=2;
int b=2;

int main()
{
a = a+b;
printf("%i",a);
getchar(); //Agora sim, está bem melhor
}

[/code]
Conclusão, use getchar() e evite o system("pause").


Vício 2 - conio.h e gotoxy, o veredito final

A biblioteca conio.h foi criada pela Borland para o compilador Turbo C na época do antigão IBM-PC da era de 16 bits. Ele foi desenhado especificamente para possibilitar um programador trabalhar de maneira confortável com a entrada/saída de dados (input/output) usando a interface DOS.
Aqui no fórum já houve uma discussão calorosa acerca do conio: https://www.hardware.com.br/comunidade/definitiva-conclusao/796311/

A questão é, apesar de ser útil em certos casos, o conio.h é uma biblioteca bem antiga, obsoleta e não é multiplataforma, as bibliotecas construídas para Windows funcionam por cima da Win32 API. Apesar disso, eu não reprovo totalmente o seu uso, pois qualquer programador pode necessitar dela em casos onde a biblioteca padrão da linguagem C não supre certas pontos como: desenhar uma letra em posições específicas da consola (com gotoxy), desenhar carateres coloridos (com cprintf), detetar eventos do teclado (com kbhit), e por aí vai.

No entanto, existem bibliotecas que claramente são superiores ao conio, como o ncurses do projeto GNU: http://www.gnu.org/software/ncurses/. Que também possui um port para Windows: http://gnuwin32.sourceforge.net/packages/ncurses.htm.
Mais informações acerca: http://en.wikipedia.org/wiki/Ncurses.


Vício 3 - Usar goto e criar código espaguete

Se seu professor alguma vez te ensinar a usar essa instrução no código C, poste aqui e faça o favor de denunciar a polícia federal gnomo.png.
O uso de goto deve ser evitado a qualquer custo dentro de seu código C/C++, pois o goto cria código espaguete (spaghetti code), ou seja, código que desobedece os princípios de programação estruturada. Vamos parar de enrolar e vou mostrar como o uso de goto pode tornar seu código ilegível, feio e impossível de se ler.
[code=rich]
#include
#include

/* Este programa usa a instrução goto para mostrar o quanto é
* repugnante o uso da mesma num código C */
int numero;
char repetir;

int main()
{
inicio:
printf("Digite um número: ");
scanf("%i",&numero);
if(numero == 0)
goto numerozero;
else if(numero < 0)
goto numeromenor;
else
goto numeromaior;
numerozero:
printf("Numero eh zero!\n");
printf("Deseja repetir? [s/n]\n");
scanf(" %c",&repetir);
if((tolower(repetir)) == 's')
goto inicio;
else
goto fim;
numeromenor:
printf("Numero eh negativo!\n");
printf("Deseja repetir? [s/n]\n");
scanf(" %c",&repetir);
if((tolower(repetir)) == 's')
goto inicio;
else
goto fim;
numeromaior:
printf("Numero eh positivo!\n");
printf("Deseja repetir? [s/n]\n");
scanf(" %c",&repetir);
if((tolower(repetir)) == 's')
goto inicio;
else
goto fim;
fim:
return(0);
}


[/code]

Me diga, código bonito né, tente compilar e executar, ele vai esperar que você digite um número e vai mostrar se o número é positivo, negativo ou neutro (zero).
Além de ser feio um programador usar o goto em C, é também anti-ético e ruim usar em projetos críticos com milhares de linhas de código, e mais, seu profissionalismo como programador pode facilmente ser posto em questão dentro de uma equipa de software.

Não use goto, a não ser que você programe em assembly (mnemônico jmp) onde é necessário (pois assembly não é uma linguagem estruturada).


Vicio 4 - Mal uso do scanf

Oh! scanf scanf meu amor, uma das maiores dores de cabeça do programadormostrando_lingua.png.
Um pouco de poesia para relaxar, pois essa parte do tutorial é muito importante, portanto preste muita atenção, muita concentração agora.

A função scanf, cujo protótipo está declarado em stdio.h, serve para efetuarmos a entrada (do teclado) formatada de dados nos nossos programas, daí o nome scan formated. É uma função muito poderosa, porém muito irritante e problemática para alguns iniciantes.

Eu classificaria os problemas básicos do uso de scanf em 3 tipos:
  • Deixar sujeira no buffer do teclado
  • Ler strings com espaço
  • Mau uso do fflush
Antes de explicar o que é o buffer do teclado também chamado de buffer de entrada, vamos dar olhada num código bem simples:
[code=rich]
#include

char carater;

int main()
{
printf("Escreve algum caráter\n");
scanf("%c",&carater);

printf("Mais um caráter\n");
scanf("%c",&carater);
}
[/code]
Antes de compilar e rodar o código acima, vamos tentar raciocinar de forma lógica como o programa acima deverá supostamente funcionar.
Declaramos uma variável carater do tipo char, que claramente usaremos para armazenar um (e só um) caráter.

O primeiro scanf vai tentar ler o primeiro caráter que você digitar e vai guardar na variável. O segundo scanf vai fazer exatamente o mesmo.
Isso é só uma dedução lógica, agora copie o código e tente compilar, você irá de certa maneira ficar surpreso com o comportamento do programa.

O primeiro scanf vai funcionar normalmente, já o segundo não vai funcionar e o programa vai terminar, será que fiquei louco ou o scanf é que está louco?

Agora chegou o momento de explicar porque é que isso ocorre, e o que é o buffer de teclado (também chamado buffer de entrada ou stdin).
Quando você invoca funções de entrada como scanf, getchar, etc, tais funções pausam a consola e esperam que você digite qualquer caráter terminado por ENTER.

Quando você digita esses dados, eles não são ainda enviados para a variável que irá guardar os mesmos. Os dados são enviados para um espaço de memória chamado buffer de teclado, isso explica o porquê de ser possível deletar os carateres com a tecla BACKSPACE.

Os dados só são realmente guardados na variável pretendida, quando você pressiona a tecla ENTER, pois essa tecla indica ao scanf que terminamos o ato de entrada de dados.
É importante saber que ENTER na memória é um caráter como qualquer outro, ele é o caráter LF(Line Feed) cujo código ASCII é igual à 10 (decimal).
http://pt.wikipedia.org/wiki/Tabela_ASCII

O que acontece basicamente no primeiro scanf é:
  • Você digita um caráter e pressiona ENTER;
  • Como só precisamos armazenar um caráter na variável, o caráter é armazenado na variável e o ENTER permanence no buffer de entrada;
Já o segundo scanf:
  • Como o buffer de entrada não está vazio (tem o ENTER lá), ele lê aquele ENTER, e como ENTER serve para terminar a entrada de dados pelo teclado, o scanf prossegue.
Vamos supor que no primeiro scanf você digite 'A'(ASCII 97) e pressione ENTER, o scanf vai armazenar os dois carateres(97,10) no buffer de teclado:
Imagem

Como o ENTER serve para sinalizar o fim da entrada de dados pelo teclado, o caráter 'A'(ASCII 97) vai ser tirado do buffer e armazenado na variável.

Quando você invocar o segundo scanf, ele vai ler o próximo carater do buffer de entrada, que é o ENTER, mais uma vez repito que o ENTER sinaliza o fim de entrada de dados, portanto o segundo scanf vai prosseguir sem dar chance de digitar-se algo na consola.

Mas, há uma solução simples para esse problema, basta usar o scanset * no scanf. Tal operador diz para o scanf ignorar qualquer coisa por exemplo:
[code=rich]
scanf("%c%*c",&carater);
[/code]

No código acima o scanf irá ler um primeiro caráter e depois vai suprimir o seguinte (que normalmente é o ENTER), quer dizer que tal caráter não será armazenado no buffer de entrada.

O operador de supressão * pode ser normalmente usado em outros especificadores de formato, por exemplo:
[code=rich]
%*i - ignora um inteiro.
%*f - ignora um float.
[/code]

É recomendável também usar o scanset ou operador de supressão * em leituras de string via scanf, exemplo:
[code=rich]
char str[32];
scanf("%s%*c",str);
[/code]
O código acima irá ler uma string e armazenar em str, o ENTER será ignorado pelo scanf, ou seja, não será armazenado no buffer de entrada.

Correção para o código problemático:
[code=rich]
#include

char carater;

int main()
{
printf("Escreve algum caráter\n");
scanf("%c%*c",&carater);

printf("Mais um caráter\n");
scanf("%c%*c",&carater);
}
[/code]

O segundo problema associado ao uso do scanf é a leitura de strings com espaço em branco, repare o código abaixo:
[code=rich]
#include

int main()
{
char str[100];

printf("Digite uma string com espaços\n");
scanf("%s%*c",str);

printf("Você digitou: %s",str);
getchar();
}

[/code]
Compile e rode o programa, digite uma string com espaços em branco, por exemplo "Ola Mundo".
Repare que o scanf só irá ler a primeira palavra da string("Ola") e irá ignorar o resto.

Resolver este problema é relativamente fácil, basta usar o scanset [^carater] que diz ao scanf ler todos os elementos de uma string (até espaços) delimitados por um caráter especificado no scanset.
O código abaixo por exemplo, faz o scanf ler toda a string e ignorar os carateres após o caráter 'a'.
[code=rich]
#include

int main()
{
char str[100];

printf("Digite uma string que tenha o caráter 'a'\n");
scanf("%[^'a']s%*c",str);

printf("Você digitou: %s",str);
getchar();
}

[/code]

Tente digitar por exemplo "Eu idolatro C", o scanf só vai ler "Eu idol", pois o scanf só leu parte da string delimitado pelo caráter 'a'.
Agora voltando ao cerne do problema inicial, nós podemos ler strings inteiras com espaços, se especificarmos ao scanf o caráter \n (que representa o ENTER ou quebra de linha) como delimitador da string que pretendemos ler.
[code=rich]
#include

int main()
{
char str[100];

printf("Digite uma string\n");
scanf("%[^\n]s%*c",str);

printf("Você digitou: %s",str);
getchar();
}

[/code]
Resolvido, recomendo ir ao final da página para dar uma olhada nos links referentes ao scanf.

O terceiro problema associado ao uso do scanf, é usar o fflush para limpar o buffer de entrada, é uma alternativa ao especificador de supressão *, pois elimina o ENTER ou qualquer outro caráter do buffer de entrada.
[code=rich]
#include

char carater;

int main()
{
printf("Escreve algum caráter\n");
scanf("%c",&carater);

fflush(stdin);

printf("Mais um caráter\n");
scanf("%c",&carater);
}
[/code]

Mas apesar do fflush resolver o problema, não é recomendável o seu uso em buffers de entrada (stdin é o buffer do teclado), pois segundo a documentação o seu efeito em buffers de entrada é indefinido, ora pode funcionar ora pode não funcionar, ora pode acontecer algo grave.


Vicio 5 - Mal uso de ponteiros

Em um mundo fantasioso de super-heróis, um homem chamado Ben Parker disse que grandes poderes implicam grandes responsabilidades. Todo o poder tem um preço.

Ponteiros são provavelmente a maior arma que a linguagem C oferece ao programador, arma que dá flexibilidade e poder a quem aprende a manusea-lá bem. E quem abusa dele paga um preço (por vezes muito caro).
O ponteiro em C é um tipo de dado extremamente simples, invés de guardar um valor normal, ele guarda um especial, o endereço de memória de uma outra variável.

Eu particularmente não tive dificuldades em aprender a usar corretamente ponteiros, mas é verídico o fato de ponteiros serem um entrave ao entendimento e uso por parte de programadores novos na linguagem C.

Sem mais enrolações, vamos à um código simples, vamos simplesmente declarar um ponteiro que armazenará o endereço de uma variável inteira.
Lembrando que o uso do operador & retorna o endereço do operando que estiver à direita.
[code=rich]
#include

int main()
{
int numero = 5;
int *ptr_numero = &numero;
}

[/code]Bem simples o código, podemos conhecer o endereço da variável apontada usando o especificador de formato %p na função printf.
[code=rich]
#include


int main()
{
int numero = 5;
int *ptr_numero = &numero;

printf("Endereco da variavel apontada: %p",ptr_numero);
getchar();
}

[/code]O endereço da variável apontada será mostrada em hexadecimal.

Vamos mais à fundo, invés de exibir o endereço da variável apontada, vamos exibir o valor da variável apontada, o operador * retorna não o endereço, mas sim o valor da variável apontada.
[code=rich]
#include

int main()
{
int numero = 5;
int *ptr_numero = &numero;

printf("Valor da variavel apontada: %i",*ptr_numero);
getchar();
}

[/code]




Parece que ponteiros não são tão complicados. E que tal tentar o código abaixo?
[code=rich]
#include

int main()
{
int numero = 5;
int *ptr_numero = numero;

printf("Valor da variavel apontada: %i",*ptr_numero);
getchar();
}

[/code]Execute, se você não receber um Segmentation Fault no Linux ou um"O programa parou de responder" no Windows, você tem muita sorte.
O que fizemos basicamente no código acima foi tentar acessar uma região inválida ou protegida da memória e exibir o conteúdo desta região.
Observe que não usamos o operador & para obter o endereço da variável, desta forma, nós simplesmente gravamos o valor da variável(5) e não o endereço da variável.

Outro exemplo de mau uso de ponteiros:
[code=rich]
#include

int main()
{
int *ptr_numero;
*ptr_numero = 13;
}

[/code]Primeiramente nós declaramos um ponteiro para inteiro, como nós não atribuímos nenhum endereço ao mesmo, então o ponteiro estará a apontar para uma região aleatória e desconhecida da memória.
A próxima linha é um desastre de código, nós tentamos guardar o valor inteiro 13 para uma região desconhecida na memória, nos piores casos o programa pode travar, e se não travar, o valor 13 será guardado em uma região desconhecida na memória.

Esse tipo de ponteiro é conhecido por ponteiro selvagem, pois o endereço por onde ele aponta é desconhecido, e se algum valor for gravado por intermédio deste tipo de ponteiro, qualquer coisa pode acontecer, algo de imprevisível.
Quando você não sabe o que atribuir à um ponteiro, simplesmente passe à ele o valor NULL, é uma técnica segura no uso de ponteiros.
[code=rich]
#include

int main()
{
int *ptr_numero = NULL;
}

[/code]Para terminar a parte de ponteiros, só queria ressaltar que temos uma enorme sorte de não usarmos sistemas DOS que trabalhavam no modo de processamento real, onde não haviam proteções contra violações de memória, ou seja, qualquer programa poderia mexer com a memória de programas vizinhos. E nos piores dos casos o sistema poderia corromper-se.

Conclusão

É isso aí pessoal, espero que tenham gostado deste artigo, criei com o objetivo de fazer com que certos programadores evitem vícios ridículos e por vezes perigosos, qualquer comentário será benvindo, quer seja de agradecimento, incentivo ou correção.


Referências

Código espaguete
http://pt.wikipedia.org/wiki/C%C3%B3digo_espaguete

Biblioteca nCurses
http://en.wikipedia.org/wiki/Ncurses

Conclusão definitiva sobre conio
https://www.hardware.com.br/comunidade/definitiva-conclusao/796311

Tutorial system("pause") e fflush(stdin)
http://forum.imasters.com.br/topic/336835-tutorial-systempause-e-fflushstdin/

Documentação do scanf no cplusplus.com
http://cplusplus.com/reference/cstdio/scanf/

Ler espaços de uma string com scanf
http://gpraveenkumar.wordpress.com/2009/06/10/how-to-use-scanf-to-read-string-with-space/

Ponteiros
http://pt.wikipedia.org/wiki/Ponteiro_(programa%C3%A7%C3%A3o)
leoisl
leoisl Novo Membro Registrado
24 Mensagens 1 Curtida
#2 Por leoisl
09/05/2013 - 17:27
Belo artigo! Apesar de usar C faz tempo, acabei aprendendo várias coisas.

Sobre o item 1, o programador pode usar funções específicas do SO em questão se ele souber o que está fazendo. Geralmente, quando queremos utilizar bibliotecas/funções específicas de um SO, fazemos compilação condicional através das macros #ifdef, #if, #endif, etc... Algo desse tipo: http://stackoverflow.com/questions/8666378/detect-windows-or-linux-in-c-c
Contudo, na questão de aprendizagem da linguagem C, o uso de system() realmente é uma prática muito ruim de programação, conforme foi dito no artigo...

O Vício 4 realmente é uma coisa muito ruim de C... Depois de ler o artigo, tenho a impressão de toda vez que tiver que fazer a leitura em um programa em que leio chars/strings, o negócio é sempre concatenar o %*c na string de formatação do scanf(). Concatenar o %*c também pode ser necessário quando você ler variáveis numéricas e, logo depois, ler um char/string:

scanf("%d", &n); //aqui também é necessário o %*c
scanf("%c%*c", &c);

E fazer essa checagem de enter sobrando no stdin não é apenas improdutivo, como chato e um grande concentrador de bugs!


*Algumas linguagens como Java, que é portável entre diferentes SO, é capaz de fazer leitura de dados de forma mais fácil e não permite a manipulação direta de ponteiros, elimina estes 5 vícios diretamente! Não falando que C é ruim, mas não sei se é bom como primeira linguagem, pois existem muitas maneiras em C de você bugar seu programa, principalmente se for iniciante...
Henry-Keys
Henry-Keys Geek Registrado
1.8K Mensagens 235 Curtidas
#5 Por Henry-Keys
10/05/2013 - 00:51
Olá leoisl..

Sobre o item 1, o programador pode usar funções específicas do SO em questão se ele souber o que está fazendo. Geralmente, quando queremos utilizar bibliotecas/funções específicas de um SO, fazemos compilação condicional através das macros #ifdef, #if, #endif, etc... Algo desse tipo:http://stackoverflow.com/questions/8...r-linux-in-c-c
Contudo, na questão de aprendizagem da linguagem C, o uso de system() realmente é uma prática muito ruim de programação, conforme foi dito no artigo...


Há casos em que não ligamos a questão de portabilidade, como usar a Win32 API e desenvolver só pra Windows, e isso não é tão mau pois é só pura preferência de um programador.
Agora usar o system("pause") é que chega a ser ridículo demais.


O Vício 4 realmente é uma coisa muito ruim de C... Depois de ler o artigo, tenho a impressão de toda vez que tiver que fazer a leitura em um programa em que leio chars/strings, o negócio é sempre concatenar o %*c na string de formatação do scanf(). Concatenar o %*c também pode ser necessário quando você ler variáveis numéricas e, logo depois, ler um char/string:
Código:
scanf("%d", &n); //aqui também é necessário o %*c scanf("%c%*c", &c);

E fazer essa checagem de enter sobrando no stdin não é apenas improdutivo, como chato e um grande concentrador de bugs!

O scanf não é a única função de entrada da ANSI C, como diz o zerocow fuja do scanf como o diabo foge da cruz.
Para a leitura de strings recomendo o fgets, pois não dá aquelas chatices de deixar o ENTER no buffer.


*Algumas linguagens como Java, que é portável entre diferentes SO, é capaz de fazer leitura de dados de forma mais fácil e não permite a manipulação direta de ponteiros, elimina estes 5 vícios diretamente! Não falando que C é ruim, mas não sei se é bom como primeira linguagem, pois existem muitas maneiras em C de você bugar seu programa, principalmente se for iniciante...

Por mais incrível que pareça, eu não vejo isso como uma desvantagem, porque o verdadeiro programador é aquele que aprende a eliminar esses vícios.
Não conheço a linguagem Java, mas se ela elimina esses vícios automaticamente, ou seja, não deixa o programador pelo menos conhecer então no fim o desempenho sempre fala mais alto (enrolei muito, mas você sabe que falo do garbage collector gnomo.png).

romululg

É sucesso Henry! Artigo bacana demais. Mais um parabéns pela iniciativa de acrescentar bons conteúdos ao nosso Fórum.

Muito Obrigado Rómulo, e não vou parar por aqui, ainda tenho outras idéias por concretizar.

@tpcvasco
Obrigado pela força, vou pedir a moderação que fixe o tópico nesta sala ou que adicione no tópico "Tópicos Importantes da sala programação".

Abraços e obrigado à todos.
© 1999-2024 Hardware.com.br. Todos os direitos reservados.
Imagem do Modal