sexta-feira, 15 de janeiro de 2010

Pequenas correções na rota

Como falei no último post da série sobre matrizes, hoje falaremos sobre algumas correções necessárias em nossos códigos.

Trabalhando com uma entrada limpa.
No post citado acima inclui um comando novo, e não expliquei o que ele fazia. Antes de ver o que ele faz, vamos ver um código simples que mostra porque ele é necessário.


#include <stdio.h>
#include <stdlib.h>
 
int main (){
 char ch;
 int i;
 
 for(i = 0; i < 10; i++){
    printf("Digite o %do caracter: ", i+1);
    scanf("%c", &ch);
 }
 getchar();
 return EXIT_SUCCESS;
}


Execute esse código e veja o que acontece.
Você conseguiu digitar os 10 valores?
Não??? Por que será?

O buffer do teclado
Quando você pressiona uma tecla (ou conjunto de teclas) em seu teclado é gerada uma informação: a tecla (ou conjunto de teclas) que foi digitada. Esta informação é armazenada em uma área da memória chamada de buffer do teclado, onde fica disponível para ser utilizada. Este buffer pode armazenar uma quantidade razoável de teclas digitadas que serão usadas assim que forem requisitadas.
No código acima, a função scanf 'recupera' o conteúdo de uma dessas teclas, e o armazena na variável ch.
Mas pararmos para pensar, veremos que para armazenar a letra a, por exemplo, na variável ch usando a função scanf, digitamos duas teclas: a tecla a e a tecla enter.
Quando a scanf 'recupera' a tecla a, a tecla enter continua no buffer. Por isso que na próxima chamada da scanf ela irá recuperar o valor da tecla enter, pulando uma entrada. Este problema é clássico e recebeu um nome: sujeira no buffer de entrada.
Existem algumas formas de resolver este problemas.
Vejamos quais são elas, da mais simples à mais drástica (rsrs).

Lendo 2 e ignorando 1.
Este subtítulo diz tudo. Observe o comando abaixo:

 scanf("%c*c", &ch);

Ele faz exatamente isto: Lê 2 caracteres, e ignora o segundo. Susbstituia a chamada da função scanf no código acima e veja a diferença.

Uma outra forma de obter este mesmo resultado é com o seguinte comando:

 scanf(" %c",&ch);

Observe o espaço entre a " e o %. Pode testar. Tem o mesmo efeito.
E se o usuário cometer o erro de digitar mais de 2 caracter antes do enter? Faça o teste.

Pequenas falhas na entrada
A técnica usada acima é pouco eficiente, como podemos ver com este último teste.
Para resolver pequenas erros como este, podemos limpar o buffer usando a função setbuf.
A função setbuf preenche um buffer informado com um valor também informado. Vamos usar o exemplo do código do post sobre matrizes:

 setbuf(stdin, NULL);

O comando acima preenche o buffer da entrada padrão (stdin) com o valor NULL.
A entrada padrão, no nosso caso, é o buffer do teclado. E a palavra NULL é uma constante padrão do C, e significa um valor nulo (um byte com todos os bits 0). Um buffer preenchido com NULL é considerado limpo/vazio.
Para pequenas falhas como uma ou duas teclas a mais esta técnica funciona bem.
É só colocar este comando na linha seguinte a função scanf original que lê apenas um caracter, desta forma:

 scanf("%c", &ch);
 setbuf(stdin, NULL);

Grandes falhas na entrada
O buffer do teclado possui uma característica interessante. Às vezes ele armazena mais do que realmente 'mostra'. Experimente usar este novo código e digitar uns 15 caracteres ou mais, por exemplo, em cada entrada.
A função setbuf limpa somente o que ela 'vê' armazenado no buffer, e logo depois desta limpeza, o buffer é novamente preenchido com o que estava 'oculto' e, desta forma, continua 'sujo'.
Para resolver isto só com 'força bruta' (é só modo de dizer, diga não à violência ;).
Dentre as muitas formas de limpar a entrada usando 'força bruta', eu, particularmente, gosto desta:

 while(getchar()!= '\n');

O comando acima lê os caracteres deixados no buffer, um a um, até encontrar um enter, e despreza todos eles, inclusive o enter.
Substitua o comando setbuf por este mostrado acima e faça o teste. Pode usar 'força bruta' na hora de testar rsrs.


Dando satisfações ao sistema operacional
Existe um outro detalhe no código que usei como exemplo neste post. É o retorno da função main.
Até o momento nós temos usado o retorno das funções para retornar valores para variáveis ou indicar se a função foi bem sucedida. Para isto tenho usado 0 para indicar falha e 1 para indicar sucesso.
A função main também possui um retorno. Em alguns códigos antigos você vai encontrar a main declarada sem retorno ou com um retorno do tipo void. Mas a um bom tempo ela é declarada por padrão com um retorno do tipo inteiro.
Este retorno indica para o sistema operacional se o programa foi concluído com ou sem falhas, ou seja, 0 indica que não houveram falhas, e qualquer número diferente de 0 indica alguma falha.
Você pode usar as constantes padrões EXIT_SUCCESS, que significa que não houve falhas, ou EXIT_FAILURE, que indica alguma falha, ou usar os números diretamente. É muito comum se deparar com um return 0.

System ou não system?
Muitas pessoas criticam o uso da função system (system("pause"); system("cls");).
Um problema que vejo no uso da função system é que ela deixa o código dependente do sistema operacional.
Um system("cls") funciona muito bem para 'limpar a tela' num ambiente Windows, mas não vai funcionar no Linux, onde seria necessário usar um system("clear"). Existem formas de evitar isto usando bibliotecas externas, que funcionam nos dois ambientes. Como o nosso objetivo aqui é aprender o C padrão, vou continuar usando esta função para 'limpar a tela'. Se você trabalha num ambiente Linux, faça a correção quando necessário.
Quanto ao system("pause");, é muito mais simples de resolver. Basta usar um getchar();. Esta função provocará uma pausa na execução do código até que um enter seja digitado, e funciona nos dois ambientes. Não será exibido "Digite qualquer tecla para continuar...", mas o resultado da execução do programa poderá ser visto normalmente. Procurarei usá-la daqui pra frente.

Até o próximo post.

terça-feira, 12 de janeiro de 2010

Matrizes. Final (Matriz de strings/alocação)

Hoje, para encerrar a nossa série sobre matrizes, vamos ver a matriz de strings.

Matriz de Strings
Uma matriz de string é uma matriz de caracteres de duas dimensões, onde a 1a dimensão é a quantidade de strings e a 2a dimensão é o tamanho máximo de cada string.

 char matrstr[5][30];

Na linha acima, definimos uma matriz capaz de armazenar 5 strings que tenham no máximo 29 caracteres cada (o último caracter é o \0, o finalizador da string).
Se quisermos nos referir a cada string individualmente, é só nos referir a uma única dimensão.

 printf("%s\n", matrstr[2]);

O comando acima exibe a 3a string da matriz.

Vejamos um código para exemplificar isto.


#include <stdio.h>
#include <stdlib.h>
const int QTDSTR = 5;
const int COMPRIMENTO = 30;
 
void preenche_matrstr(char matriz[][30]);
 
int main (){
 char matrstr [5][30];
 int i, opcao;
 
 preenche_matrstr(matrstr);
 do{
    printf("Escolha uma das opcao abaixo:\n\n");
    printf("1. Exibir posicoes pares\n");
    printf("2. Exibir posicoes impares\n");
    printf("3. Sair\n\nopcao: ");
    scanf("%d", &opcao);
    switch(opcao){
       case 1:
          for(i = 0; i < QTDSTR; i++)
             if(!((i+1)%2))
                printf("palavra %d: %s\n", i+1, matrstr[i]);
          break;
       case 2:
          for(i = 0; i < QTDSTR; i++)
             if((i+1)%2)
                printf("palavra %d: %s\n", i+1, matrstr[i]);
          break;
       case 3:
          break;
       default:
          printf("OPCAO INVALIDA!!! Digite novamente.\n");
    }
    system("pause");
    system("cls");
 }while(opcao != 3);
}
 
void preenche_matrstr(char matriz[][30]){
 int i;
 
 printf("PREENCHENDO UMA MATRIZ DE STRINGS:\n\n");
 for(i = 0; i < QTDSTR; i++){
    printf ("Digite uma palavra para a posicao %d: ", i+1);
    gets(matriz[i]);
 }
 system("cls");
}


Este código preenche uma matriz de strings com palavras digitadas pelo usuário, e depois apresenta um menu onde ele escolhe se deseja ver as palavras nas posições pares ou ímpares.
As matrizes de strings são conceitos muito simples. Mas que podem ficar muito complicados se incluirmos ponteiros na brincadeira.

Ponteiros x Matrizes
Você deve lembrar de quando apresentei o conceito de matriz (aqui), como sendo um vetor de vetores.
Também deve estar lembrado sobre a forma como alocamos um vetor dinamicamente usando a função malloc e um ponteiro (aqui). Portanto, a declaração de um vetor com o comando abaixo é muito comum:

 char* str;

E o que você diria ao se deparar com a declaração abaixo:

 char** matrstr;

Ela assusta no início. Mas depois que conhecemos o seu significado fica fácil (assim eu espero rsrs).

Apontando para quem já está apontando
O comando acima declara um ponteiro para um ponteiro de char (???). Antes que você pense que usando ele estamos declarando uma matriz, eu já quero deixar bem claro que não. O comando acima não declara uma matriz.

Observe o código abaixo:


#include <stdio.h>
#include <stdlib.h>
 
int main(){
 char** matriz;
 char matrstr[5][30];
 
 printf("O tamanho de char* matriz eh %d.\n", sizeof(matriz));
 printf("O tamanho de char matrstr[1][1] eh %d.\n", sizeof(matrstr));
 system("pause");
}


Como demonstrado no código acima, ao receber uma declaração de um ponteiro para ponteiro (ou mesmo um ponteiro simples), o compilador reversa espaço para armazenar um endereço (aqui na minha máquina esse espaço é de 4 bytes). Já quando recebe a declaração de uma matriz, ele reserva espaço para toda a matriz. Neste caso especificamente, são reservados 150 bytes.
Mas, assim como podemos tratar um endereço para um bloco de memória como um vetor, podemos fazer o mesmo com uma matriz, como inclusive já vimos na 2a parte desta série. (A diferença no post de hoje é que vamos expandir um pouco mais o conceito.)
Para isso, devemos alocar dinamicamente um bloco de memória com tamanho suficiente para armazenar nossa matriz inteira, e depois alocar, dentro deste bloco, os espaços para conter cada 'linha' da nossa matriz individualmente.
Vamos ao código:


#include <stdio.h>
#include <stdlib.h>
 
char** preenche_matrstr(int*, int*);
 
int main (){
 char **matrstr;
 int qtdstr, tamanho, i, opcao;
 
 matrstr = preenche_matrstr(&qtdstr, &tamanho);
 do{
    printf("Escolha uma das opcao abaixo:\n\n");
    printf("1. Exibir posicoes pares\n");
    printf("2. Exibir posicoes impares\n");
    printf("3. Sair\n\nopcao: ");
    scanf("%d", &opcao);
    switch(opcao){
       case 1:
          for(i = 0; i < qtdstr; i++)
             if(!((i+1)%2))
                printf("palavra %d: %s\n", i+1, *(matrstr+i));
          break;
       case 2:
          for(i = 0; i < qtdstr; i++)
             if((i+1)%2)
                printf("palavra %d: %s\n", i+1, matrstr[i]);
          break;
       case 3:
          break;
       default:
          printf("OPCAO INVALIDA!!! Digite novamente.\n");
    }
    system("pause");
    system("cls");
 }while(opcao != 3);
 for(i = 0; i < *qtd; i++){
    free(matrstr[i]);
 free(matrstr);
}
 
char** preenche_matrstr(int* qtd, int* tam){
 char** matriz;
 int i;
 
 printf("Digite a quantidade de strings da matriz: ");
 scanf("%d", qtd);
 printf("Digite o tamanho maximo de cada string: ");
 scanf("%d", tam);
 
 matriz = (char **) malloc (*qtd);
 if (!matriz){
    printf("NAO FOI POSSIVEL ALOCAR A MATRIZ.\n");
    exit(0);
 }
 else
    for(i = 0; i < *qtd; i++){
       *(matriz + i) = (char*) malloc (*tam);
       if (!(*(matriz + i))){
          printf("NAO FOI POSSIVEL ALOCAR A STRING %d.\n", i+1);
          exit(i);
       }
    }
 
 printf("PREENCHENDO UMA MATRIZ DE STRINGS:\n\n");
 setbuf(stdin, NULL);
 for(i = 0; i < *qtd; i++){
    printf ("Digite uma palavra para a posicao %d: ", i+1);
    gets(matriz [i]);
 }
 system("cls");
 return matriz;
}


Como a matriz será armazenada dinamicamente, demos ao usuário a possibilidade de escolher quantas serão e qual o tamanho máximo das strings da matriz.
Observe que em alguns pontos do código eu usei matriz[i] e em outros eu usei *(matriz+i). Foi simplesmente para mostrar que os 2 são equivalentes. Não é prático, misturar duas notações diferentes em um mesmo código. Escolha uma delas e use em todo o código.

OBS: Existe um comando novo neste código (setbuf), mas não comentarei sobre ele agora. No próximo post falaremos detalhadamente sobre ele.

Até o próximo post.

domingo, 10 de janeiro de 2010

Matrizes. Parte III (transposta/determinante)

Agora que já conhecemos as matrizes, vamos ver algumas operações com matrizes.

Transpondo uma matriz quadrada
Vamos começar criando uma matriz transposta de uma matriz quadrada 3x3. Não sabe o que é matriz transposta? Veja aqui.
Olhando a primeira vista podemos pensar que seria só trocar a posição dos elementos. Algo que o fragmento de código abaixo faria sem problemas.


 for(i = 0; i < colunas; i++)
    for(j = 0; j < colunas; j++){
       aux = mat[i][j];
       mat[i][j] = mat[j][i];
       mat[j][i] = aux;
 }


Se você tiver a curiosidade pode implementar este código. E vai ter uma triste surpresa (rsrs).
Será que você pode dizer por que este código não funciona?

Como você já descobriu, este código funciona (compila e executa), mas não faz o que se esperava que fizesse. Na verdade, ele troca a posição dos elementos do vetor e, depois, os retorna para as posições iniciais. Ele não sabe quando os elementos não precisam ser trocados, ou porque já foram, ou porque realmente não é necessário, como é o caso dos elementos da diagonal principal da matriz.
No caso da diagonal principal é fácil, basta saber se o número da coluna é igual ao da linha. E no outros casos? Como evitar que a troca seja feita novamente, fazendo o elemento voltar para a sua posição inicial?
Vamos fazer um teste de mesa (ou chinês, como preferirem) e ver como as trocas são feitas.
Linha = 0, Coluna = 0 => Diagonal principal, não precisa trocar;
Linha = 0, Coluna = 1 => Troca o elemento com o da Linha = 1, Coluna = 0;
Linha = 0, Coluna = 2 => Troca o elemento com o da Linha = 2, Coluna = 0;

Linha = 1, Coluna = 0 => A troca já foi feita, não precisa trocar;
Linha = 1, Coluna = 1 => Diagonal principal, não precisa trocar;
Linha = 1, Coluna = 2 => Troca o elemento com o da Linha = 2, Coluna = 1;
Linha = 2, Coluna = 0 => A troca já foi feita, não precisa trocar;
Linha = 2, Coluna = 1 => A troca já foi feita, não precisa trocar;
Linha = 2, Coluna = 2 => Diagonal principal, não precisa trocar;

Observando as linhas em negrito, que mostram as trocas que precisam ser realmente feitas, podemos encontrar algo em comum entre elas. Já percebeu?

O número da linha é menor que o da coluna! Agora ficou fácil, né?
Veja abaixo como fica o código completo que faz a transposição de uma matriz quadradra 3x3.


#include <stdio.h>
#include <stdlib.h>
 
const int LINHAS = 3;
const int COLUNAS = 3;
 
void preenche_mat_int(int [][3]);
void exibe_mat_int(int [][3]);
void transpor_mat_int(int [][3]);
 
int main(){
 int matriz[3][3];
 
 preenche_mat_int(matriz);
 exibe_mat_int(matriz);
 transpor_mat_int(matriz);
 exibe_mat_int(matriz);
 system("pause");
}
 
int ler_int(){
 int digitado;
 
 printf("Digite um valor inteiro: ");
 scanf("%d",&digitado);
 return digitado;
}
 
void mostra_int(int num){
 printf("%d ",num);
}
 
void preenche_mat_int(int mat[][3]){
 int i, j;
 
 printf("\nPREENCHENDO A MATRIZ:\n");
 for(i = 0; i < LINHAS; i++)
    for(j = 0; j < COLUNAS; j++)
       mat[i][j] = ler_int();
}
 
void exibe_mat_int(int mat[][3]){
 int i, j;
 
 printf("\n");
 for(i = 0; i < LINHAS; i++){
    for(j = 0; j < COLUNAS; j++)
       mostra_int(mat[i][j]);
    printf("\n");
 }
 printf("\n\n");
}
 
void transpor_mat_int(int mat [][3]){
 int i, j, aux;
 
 printf("\nTRANSPOSICAO DA MATRIZ:\n");
 for(i = 0; i < LINHAS; i++)
    for(j = 0; j < COLUNAS; j++)
       if((i != j) && (i < j)){
          aux = mat[i][j];
          mat[i][j] = mat[j][i];
          mat[j][i] = aux;
       }
}


Determinante de uma matriz quadrada 2x2
Outra função comum no trabalho com matrizes é o cálculo do seu determinante. Veja o que é, e como é feito aqui.
O código abaixo calcula o determinante de uma matriz quadrada 2x2.


#include <stdio.h>
#include <stdlib.h>
 
const int LINHAS = 2;
const int COLUNAS = 2;
 
void preenche_mat_int(int [][2]);
void exibe_mat_int(int [][2]);
int determ_mat_int_2x2(int [][2]);
 
int main(){
 int matriz[2][2], det;
 
 preenche_mat_int(matriz);
 exibe_mat_int(matriz);
 det = determ_mat_int_2x2(matriz);
 printf("O determinante da matriz informada = %d\n", det);
 system("pause");
}
 
int ler_int(){
 int digitado;
 
 printf("Digite um valor inteiro: ");
 scanf("%d",&digitado);
 return digitado;
}
 
void mostra_int(int num){
 printf("%d ",num);
}
 
void preenche_mat_int(int mat[][2]){
 int i, j;
 
 printf("\nPREENCHENDO A MATRIZ:\n");
 for(i = 0; i < LINHAS; i++)
    for(j = 0; j < COLUNAS; j++)
       mat[i][j] = ler_int();
}
 
void exibe_mat_int(int mat[][2]){
 int i, j;
 
 printf("\n");
 for(i = 0; i < LINHAS; i++){
    for(j = 0; j < COLUNAS; j++)
       mostra_int(mat[i][j]);
    printf("\n");
 }
 printf("\n\n");
}
 
int determ_mat_int_2x2(int mat[][2]){
 
 return (mat[0][0] * mat[1][1]) - (mat[0][1] * mat[1][0]);
}


É possível calcular o determinante de uma matriz quadrada de qualquer ordem, mas para isto precisamos ver um conceito que ainda não vimos. Voltarei a este assunto mais a frente.

Até o próximo post.

sábado, 9 de janeiro de 2010

Matrizes. Parte II (funções/alocação)

Continuando nossa série sobre matrizes, hoje vamos ver algumas operações básicas com matrizes.

Matrizes x Funções
Por serem intimamente relacionadas com os vetores, a operações entre matrizes e funções é praticamente idêntica, salvo o fato de termos uma (ou muitas) dimensão(ões) a mais.
Vamos ao código e você vai entender.


#include <stdio.h>
#include <stdlib.h>
 
const int LINHAS = 2;
const int COLUNAS = 2;
 
void preenche_mat_int(int [2][2]);
void exibe_mat_int(int [][2]);
 
int main(){
 int matriz[2][2];
 
 preenche_mat_int(matriz);
 exibe_mat_int(matriz);
 system("pause");
}
 
int ler_int(){
 int digitado;
 
 printf("Digite um valor inteiro: ");
 scanf("%d",&digitado);
 return digitado;
}
 
void mostra_int(int num){
 printf("%d ",num);
}
 
void preenche_mat_int(int mat[2][2]){
 int i, j;
 
 printf("\nPREENCHENDO A MATRIZ:\n");
 for(i = 0; i < LINHAS; i++)
    for(j = 0; j < COLUNAS; j++)
       mat[i][j] = ler_int();
}
 
void exibe_mat_int(int mat[][2]){
 int i, j;
 
 printf("\n");
 for(i = 0; i < LINHAS; i++){
    for(j = 0; j < COLUNAS; j++)
       mostra_int(mat[i][j]);
    printf("\n");
 }
 printf("\n\n");
}


O código acima tem bastante novidade!
A 1a delas é o uso de protótipos de função. Eu já deveria ter falado sobre eles, quando falei sobre funções (aqui), mas nunca é tarde (rsrs).

Faça sem saber como.
Em todos os códigos que vimos até agora usando funções, sempre criamos as funções antes de chamá-las na main. O código da nossa função main era sempre o último e, assim, as funções já eram 'conhecidas' dela.
No entanto é possível criar a main antes das outras funções. O único requisito é informar como essas funções serão chamadas, quais o seus retornos e parâmetros.
Esta é a função dos protótipos. Eles se parecem com o cabeçalho das funções, exceto que o nome dos parâmetros pode ser omitido.

A 2a novidade, que já aparece nos protótipos, é a forma de passar uma matriz para uma função. Deve ser informado o tipo de dados contido na matriz e o tamanho de suas dimensões. O tamanho da 1a dimensão pode ser omitido, somente da 1a. Se a matriz tiver 3 dimensões, por exemplo, o tamanho das 2a e 3a dimensões tem que ser informado.
A última é a forma de 'varrer' a matriz. Usamos um for para 'varrer' as linhas e, dentro dele, um outro for para 'varrer' todas as colunas desta linha antes de passar para a próxima. Por isso a necessidade de saber quantas linhas e colunas a matriz tem.
Como as dimensões da matriz já são conhecidas é uma ótima prática criar constantes globais que armazenem estes valores, o que facilita o acesso a elas em qualquer parte do programa.

Funções x Matrizes
Da mesma forma que um vetor pode ser alocado dinâmicamente, uma matriz também pode ser. Observe o código abaixo:


#include <stdio.h>
#include <stdlib.h>
 
int ler_int(){
 int digitado;
 
 printf("Digite um valor inteiro: ");
 scanf("%d",&digitado);
 return digitado;
}
 
void mostra_int(int num){
 printf("%d ",num);
}
 
int* preenche_mat_int(int linhas, int colunas){
 int *mat=0, i, j;
 
 if ((linhas * colunas) > 0)
    mat = (int *) malloc (linhas * colunas * sizeof(int));
 if (mat){
    printf("\nPREENCHENDO A MATRIZ:\n");
    for(i = 0; i < linhas; i++)
       for(j = 0; j < colunas; j++)
          *(mat + (i * colunas) + j) = ler_int();
 }
 else
    printf("NAO FOI POSSIVEL ALOCAR A MATRIZ!!!\n");
 return mat;
}
 
void exibe_mat_int(int* mat, int linhas, int colunas){
 int i, j;
 
 printf("\n");
 for(i = 0; i < linhas; i++){
    for(j = 0; j < colunas; j++)
       mostra_int(*(mat + (i * colunas) + j));
    printf("\n");
 }
 printf("\n\n");
}
 
int main(){
 int *matriz, linhas, colunas;
 
 printf("Digite o numero de linhas da matriz (maior que zero):\n");
 linhas = ler_int();
 printf("Digite o numero de colunas da matriz(maior que zero):\n");
 colunas = ler_int();
 matriz = preenche_mat_int(linhas, colunas);
 if(matriz){
    exibe_mat_int(matriz, linhas, colunas);
    free(matriz);
 }
 system("pause");
}


A diferença básica neste código é a permitir ao usuário que defina o número de linhas e colunas da matriz, por isto não pudemos armazenar estes valores em constantes globais (observe que tivemos que passar estes valores para todas as funções). Uma vez definidas, linhas e colunas, verificamos se os valores informados foram válidos e, alocamos espaço para a matriz.
Como o bloco de memória alocado não foi definido como uma matriz, ele não pode ser indexado como uma, usando os []. A solução para isso é o uso de ponteiros e da aritmética de ponteiros.
A linha 25 mostra como isto é feito. Observe a multiplicação de i, que contém o número da linha atual, pela quantidade de colunas. Dá pra entender o porquê, certo?

Por hoje é isto. Até o próximo post.

sexta-feira, 8 de janeiro de 2010

Matrizes. Parte I (conceitos)

Hoje vamos começar a falar sobre um tipo de vetor especial: um vetor de vetores, a matriz.

Vetor de vetores?
Como já sabemos, vetor é um agrupamento sequencial de variáveis do mesmo tipo. O mais comum deles é um deste tipo que você está lendo aqui: um vetor de caracteres (as famosas strings). Confirmando o conceito: várias letras (olha o mesmo tipo aí) em sequência (olha a sequência aí). Podemos ter vetores de vários tipos de dados, inclusive de outros vetores.
Vou desenhar, assim você vai entender melhor.



Na figura acima, temos a representação de como é armazenado em memória um vetor que está armazenando outros 4 vetores.
Na verdade, esta divisões internas não existem(???). Este vetor de vetores, corresponde a um único vetor com 16 elementos. Então qual a razão dessa divisão?
A resposta é PRATICIDADE.
Lembra do lema 'Dividir para conquistar' que vimos aplicado às funções? Esta é a sua aplicação nos vetores. É muito mais fácil, visualizar cada pequeno bloco de cada vez do que todo o conjunto. Tanto é assim, que apesar deste vetor ser armazenado como na figura acima, nos referimos a ele, como se estivesse armazenado como na figura abaixo:



Agora passamos a enxergar o nosso vetor de vetores na forma de linhas, onde cada 'linha' armazena um vetor.
Olhando para esta representação, podemos notar que os elementos dos vetores armazenados nas 'linhas', parecem formar 'colunas'. Então, com o intuito de, mais uma vez, facilitar nossa visualização, chegamos a representação do nosso vetor de vetores de uma forma, espero, conhecida: a matriz. Observe a figura abaixo:



Esta é uma matriz de duas dimensões (linhas e colunas, ou eixos x e y). Não existem limites para o número de dimensões que uma matriz possa ter, embora quanto mais dimensões, mais trabalhosa é a operação com a matriz. As mais comuns são as de duas e três dimensões (páginas, linhas e colunas, ou eixos x, y e z).
Uma representação de uma matriz de 3 dimensões seria como na figura abaixo:



Agora que já vimos como uma matriz é armazenada e representada, vamos ver como é o seu uso em C.

As matrizes e o C
Para definir uma matriz em C, devemos informar o seu tipo, nome e os tamanhos de cada uma das suas dimensões.

 <tipo> nome [tamanho_dimensão1] [tamanho_dimensão2]...[tamanho_dimensãoN];

Para a matriz representada pelas figuras acima, uma matriz com 4 linhas e 4 colunas, que é do tipo inteiro, por exemplo, o comando seria:

  int matriz [4][4];

Assim, para atribuir o valor do elemento da 2a linha e 3a coluna, por exemplo, a uma variável inteira num, o comando é:

  num = matriz[1][2];

Assim como os vetores, o índice base de uma matriz também é 0 (zero). Então, a 2a linha é a de índice 1, e a 3a coluna é a de índice 2.

Fácil, não.
No próximo post veremos algumas operações com matrizes. Até lá.

domingo, 3 de janeiro de 2010

Funções x Vetores. Parte Final (código)

Agora que já vimos os conceitos e como funciona a alocação dinâmica, estamos prontos para ver um código exemplo. Então vamos a ele.


#include <stdio.h>
#include <stdlib.h>
 
int ler_int(){
  int digitado;
 
 printf("Digite um valor inteiro: ");
 scanf("%d",&digitado);
 return digitado;
}
 
void mostra_int(int num){
 printf("%d ",num);
}
 
void exibe_vetor(int* vet, int tamanho){
 int i;
 
 printf("\n");
 for(i = 0; i < tamanho; i++)
    mostra_int(vet[i]);
 printf("\n\n");
}
 
int* preenche_vet_int(int tamanho){
 int *vet=0, i;
 
 vet = (int *) malloc (tamanho * sizeof(int));
 if (vet){
    printf("\nPREENCHENDO O VETOR:\n");
    for(i = 0; i < tamanho; i++)
       vet[i] = ler_int();
 }
 return vet;
}
 
int aumenta_vet_int(int* vet, int* tamanho, int incremento){
 int novo_tam, i;
 
 novo_tam = *tamanho + incremento;
 vet = (int *) realloc (vet, novo_tam * sizeof(int));
 if (vet){
    printf("\nPREENCHENDO OS NOVOS ITENS DO VETOR:\n");
    for(i = *tamanho; i < novo_tam; i++)
       vet[i] = ler_int();
    *tamanho = novo_tam;
    return 1;
 }
 else{
    printf("NAO FOI POSSIVEL AUMENTAR O VETOR!\n");
    return 0;
 }
}
 
int diminui_vet_int(int* vet, int* tamanho, int decremento){
 int novo_tam, i;
 
 novo_tam = *tamanho - decremento;
 if (novo_tam <= 0){
    printf("DECREMENTO INFORMADO INVALIDO!!! NAO FOI POSSIVEL DIMINUIR O VETOR!\n");
    return 0;
 }
 vet = (int *) realloc (vet, novo_tam * sizeof(int));
 if (vet){
    *tamanho = novo_tam;
    return 1;
 }
 else{
    printf("NAO FOI POSSIVEL DIMINUIR O VETOR!\n");
    return 0;
 }
}
 
int main(){
 int *vetor, tamanho, incremento, decremento;
 
 printf("Digite o tamanho do vetor:\n");
 tamanho = ler_int();
 vetor = preenche_vet_int(tamanho);
 if (vetor){
    exibe_vetor(vetor, tamanho);
    printf("Digite o quanto deseja aumentar o vetor:\n");
    incremento = ler_int();
    if (aumenta_vet_int(vetor, &tamanho, incremento))
       exibe_vetor(vetor, tamanho);
    printf("Digite o quanto deseja diminuir o vetor:\n");
    decremento = ler_int();
    if (diminui_vet_int(vetor, &tamanho, decremento))
       exibe_vetor(vetor, tamanho);
    free(vetor);
 }
 else
    printf("NAO FOI POSSIVEL ALOCAR O VETOR!\n");
 system("pause");
}


Depois do que já vimos nos post anteriores, este código ficou muito simples.
Algumas funções já são conhecidas nossas. E dentre as novas funções existe pouca coisa que ainda não foi dita.
A primeira função nova está na linha 25, e é a razão desta sequência de posts. É uma função que retorna um vetor. Um detalhe interessante dela é que damos ao usuário a liberdade de definir o tamanho deste vetor. Seu funcionamento é simples.
Declaramos um ponteiro para inteiros vet e o inicializamos com o valor 0. Este ponteiro irá armazenar o endereço do vetor que será alocado dinamicamente.
Fazemos uma chamada a função malloc com o tamanho definido pelo usuário, e atribuimos o seu retorno ao ponteiro vet.
Se a chamada teve sucesso e o vetor foi alocado, imediatamente vamos preenchê-lo.
Observe o uso do if sem nenhum operador relacional. Na verdade ele verifica o valor de vet. Por isso o inicializamos com 0. Se a função malloc falhou, o valor de vet ainda será 0 (se funcionar perfeitamente, malloc nunca retorna um endereço 0). Este if verifica se o valor de vet é diferente de 0 (cuidado com isto: por diferente entenda qualquer outro valor, inclusive negativo). Este uso do if é muito comum, e passaremos a usá-lo, sempre que possível, daqui pra frente, como pode ser visto nas linhas 84 e 88, onde ele avalia o valor retornado pelas funções que aumentam e diminuem o tamanho do vetor, respectivamente.
A segunda nova função, na linha 37, aumenta o tamanho do vetor com um valor também definido pelo usuário, usando a função realloc. Observe que usando um ponteiro para a variável tamanho, podemos alterar o seu valor dentro da própria função.
A última função nova, na linha 55, diminui o tamanho do vetor de forma semelhante à função que o aumenta, usando a função realloc com um valor definido pelo usuário. A diferença entre elas é a verificação da validade do valor informado pelo usuário, evitando um erro ao tentar alterar o tamanho do vetor para 0 ou um valor negativo.
Observe que na linha 90, já na função main, desalocamos o bloco de memória usado pelo vetor. Isto é muito importante, se não este espaço ficará 'ocupado' mesmo após o término do programa.

Com isto encerramos a nossa série sobre funções que retornam vetores e como isto é possível utilizando a alocação dinâmica.
Alguma dúvida?

sábado, 2 de janeiro de 2010

Funções x Vetores. Parte II (alocação dinâmica)

Hoje vamos ver mais a fundo os conceitos que falamos no post anterior.

Alocando memória em C
A função especifica em C para alocar memória é a malloc (memory allocation). Esta função aloca (reserva) uma área sequêncial na memória heap com a quantidade de bytes especificada, e retorna o endereço da mesma. A sua sintaxe é esta:

 malloc (tamanho);

onde tamanho é a quantidade de bytes que se deseja alocar.

No entanto, devemos lembrar, que os tipos de dados básicos do C ocupam mais de 1 byte, com a excessão do tipo char. Como não temos como precisar quantos bytes exatamente cada computador (micro, celular, etc.) utiliza para armazenar cada um destes tipos básicos, é necessário fazer uma operação chamada de conversão de tipo (type casting) explícita.

Convertendo tipos
Já vimos que a divisão de 2 inteiros, nem sempre retorna 1 outro inteiro (por exemplo: 2/3).
Para resolver este problema, a solução mais simples seria declarar as variáveis envolvidas nesta divisão como floats ao invés de inteiros. Mas há casos em que esta alteração de tipos nem sempre é possível, como é o caso da função malloc, por exemplo.
Esta função retorna, a grosso modo, o endereço de um vetor de char com a quantidade especificada. Não temos como alterar esta função, mas temos como alterar o seu retorno.
Veja no código abaixo como podemos resolver a questão da divisão usando uma conversão de tipos.


#include "stdio.h"
#include "stdlib.h"
 
int main(){
 int a = 3, b = 2;
 float c;
 
 c = (float)a/b;
 printf("%.2f\n", c);
 system("pause");
}


Observe a linha 8. Esta é a forma de converter um tipo explicitamente em C. Neste caso estamos alterando o tipo da variável a de inteiro para float, o que garante que o resultado da divisão também será um float.

Voltando à alocação dinâmica...
Agora que sabemos como é feita uma alteração de tipos em C, podemos alterar a forma de chamar a função malloc.

 (<tipo de retorno> *) malloc (tamanho * sizeof(<tipo que será armazenado>);

Duas coisas chamam a nossa atenção no comando acima.
A primeira é o * dentro dos () que fazem a alteração do tipo de retorno. Ele é necessário porque a função malloc não retorna um tipo básico, mas sim um endereço que aponta para um bloco de memória que irá armazenar variáveis deste tipo básico, ou seja, um ponteiro para este bloco.
A segunda é o uso da função sizeof dentro dos () onde é informado o tipo dos dados que serão armazenados. A função sizeof nos retorna o número de bytes necessários, no computador utilizado, para armazenar o tipo de dado especificado.
Se quisermos alocar um espaço suficiente para conter 30 inteiros e atribuir este espaço a um vetor, os comandos usados seriam:

 int* vet;
 vet = (int *) malloc (30 * sizeof(int));

E se o espaço ainda não for suficiente?
Não haveria sentido em se chamar alocação dinamica, se não pudéssemos alterar dinamicamente o tamanho do vetor. E como fazer isto?
A resposta está na função realloc (reallocation). Ela é funciona básicamente como a função malloc. A diferença é que ela altera o tamanho de um bloco já alocado pela função malloc. Observe bem isto! Não é possível alterar o tamanho de um bloco com a função realloc se antes ele não tiver sido alocado com a função malloc!
Para alterar o tamanho do vetor do comando acima, de 30 para 50 inteiros, o comando seria este:

 vet = (int *) realloc (vet, 50 * sizeof(int));

Observe que a diferença básica é que informamos qual o bloco que será alterado dentro dos () antes de informarmos o novo valor.
Um detalhe interessante, é que se já houverem valores armazenados no bloco/vetor que terá seu tamanho alterado, eles não serão perdidos, desde que o bloco tenha o seu tamanho aumentado. Se o tamanho for reduzido, serão perdidos os valores armazenados no final do bloco.

Recolhendo o lixo.
Nós vimos que as variáveis locais só existem enquanto as funções estão em execução, e que depois disso elas são eliminadas. As variáveis globais só existem enquanto o programa está em execução, e depois disto elas também são eliminadas. Já com as variáveis alocadas dinamicamente isto não ocorre.
Estas variáveis ficam na memória mesmo depois que todas funções são encerradas, inclusive a main. Isso quer dizer que elas ficam na memória mesmo depois que o programa termina. Isto causa uma perda de memória. Dependendo da quantidade de memória disponível no sistema isto pode fazer com que o sistema pare.
(Algumas linguagens de programação se encarregam de eliminar essas variáveis quando o programa se encerra. Isto é chamado de coletor de lixo (garbage colletor). Não é o caso de C/C++.)
Só existem duas formas de recuperar esta memória.
A primeira forma, e mais drástica, é reiniciando o sistema. Mas, assim que o programa for executado novamente, a memória será perdida novamente.
A segunda forma, e correta, é usando a função free.
A função free desaloca um bloco de memória alocado pela função malloc, marcando esta área como memória livre.
Para desalocar o vetor que estamos usando como exemplo, o comando seria este:

 free(vet);

No próximo e, acredito, último post desta série, veremos isto funcionando na prática. Até lá.

sexta-feira, 1 de janeiro de 2010

Funções x Vetores. Parte I (conceitos)

Antes de mais nada, gostaria de desejar um 2010 de muitas realizações e conquistas!

Vamos começar o ano falando sobre algo muito importante.
Eu estava esperando alguém perguntar. Mas já que ninguém perguntou... Eu pergunto.
É possível que uma função retorne um vetor?

A resposta é SIM e NÃO (rsrs).

Vamos entender o porquê disto.

No exemplo sobre vetores e funções que vimos até agora (aqui) sempre declaramos o vetor na função main e passamos por referência para as outras funções.
O que aconteceria se fizermos o contrário?
NÃO FUNCIONARIA!

Quando declaramos uma variável dentro de uma função (inclusive vetores), ela é local à função e não é visível por nenhuma outra função e, mais ainda, ela só existe enquanto a função está 'ativa'.
Mas por que funciona com a função main?
A função main é especial. Ela é quem coordena o funcionamento de todas as outras funções, e é a única função que está disponível do início ao fim da execução do programa (podemos ter várias chamadas a outras funções, mas a main só é chamada uma única vez, no início do programa, que começa por ela). Uma variável declarada na main (inclusive vetores) pode ser passada para qualquer outra função por referência, se assim for a vontade do programador.
Uma forma de contornar esta limitação imposta pela modularidade seria armazenando as variáveis em um local onde elas pudessem ser acessadas mesmo sendo declaradas dentro de uma função. Isso existe!
Para entender como isso é feito temos que explicar como o C trata a memória do computador. Observe a figura abaixo:



Olhando a figura podemos notar que o C 'divide' a memória que é usada por um programa em 3 partes distintas.
- A pilha, onde são armazenadas as variáveis locais;
- A memória estática, onde são armazenadas as variáveis globais;
- O heap, onde são armazenadas as variáveis alocadas dinamicamente. (???)


Variáveis alocadas dinamicamente
Imagine que tenhamos que armazenar um vetor que não sabemos que tamanho terá? Já vimos que geralmente se escolhe um tamanho que se considere razoável, que na grande maioria dos casos, é maior que o tamanho necessário.
Para resolver isto foi criada a alocação dinâmica.
Alocação dinâmica nada mais é que obter espaço na memória para uma variável (leia-se estrutura de dados) a medida que ele for necessário.
Como vimos na figura acima, o espaço onde essas variáveis ficam armazenadas é um espaço separado.
Como já falamos, se sabemos o endereço de uma variável podemos ter acesso à ela, e o nome de um vetor 'corresponde' ao seu endereço.
No próximo post veremos como isto é feito na prática. Até lá

sábado, 26 de dezembro de 2009

Um vetor especial... Final (string/funções)

Como já disse o objetivo do C em tratar o vetor de caracteres (string) de uma forma especial é para facilitar nossa vida. Vamos ver isto hoje.

Funções especiais para um vetor especial.
Existem uma série de funções para trabalhar com strings em C que, em sua maioria, estão na biblioteca string.h.
Vamos conhecer as mais comuns e as facilidades que elas trazem.

Obtendo uma string (gets)
Até agora usamos a função scanf para obter dados inseridos pelo usuário.
Poderíamos continuar usando scanf para strings também, desta forma abaixo:

 char nome[21];
 scanf("%s",&nome);

Primeiro declaramos um vetor de caracteres chamado nome capaz de armazenar uma string de até 20 caracteres. Depois usamos a função scanf para armazenar neste vetor os caracteres digitados pelo usuário. Observe o uso do especificador de tipo "%s", que se refere a string.
Entretando o C possui uma função especializada para esta obtenção dos dados digitados pelo usuário: gets. Olhe o seu uso abaixo:

 char nome[21];
 gets(nome);

Bem menos digitação, o que significa uma economia de dedos (rsrs).
Ao contrário do que você possa pensar, a função gets está na biblioteca stdio.h e não na string.h.

Verificando o tamanho de uma string (strlen)
Quando definimos um vetor nem sempre sabemos se ele será totalmente preenchido. Na maioria dos casos, definimos um tamanho considerado razoável, que pode ser muito maior que o tamanho que será efetivamente usado. No exemplo acima utilizamos um vetor capaz de armazenar 20 caracteres. Se o usuário digitar um nome como ANA, por exemplo, que tem 3 caracteres nosso programa 'achará' que os 20 caracteres foram preenchidos. Poderíamos achar o tamanho efetivamente ocupado no nosso vetor fazendo um for e incrementando uma variável contadora até que o caracter \0 fosse encontrado.
No entanto o C facilita a nossa vida com uma função própria para isto a strlen. Observe o seu uso abaixo:

 int tamanho;
 tamanho = strlen(nome);

Ela retorna um inteiro com o número de caracteres efetivamente ocupado no nosso vetor.

Comparando strings (strcmp)
Imagine que você tenha que comparar 2 vetores. Seria necessário, avaliar cada item de um vetor e compará-lo com o respectivo item do outro vetor. Além disso, os vetores podem ter tamanhos diferentes.
No caso de strings, poderíamos verificar o tamanho de cada uma delas e, se fossem do mesmo tamanho, comparar os seus caracteres individualmente. Seria uma solução.
Mas não precisamos fazer isto. Temos uma função que faz isso para nós: strcmp. Observe o seu uso:

 char outro_nome[21];
 int result;
 result = strcmp(nome, outro_nome);

Esta função devolve um valor inteiro que corresponde ao resultado da comparação entre as duas strings da seguinte forma:

- result será menor que 0 (ou seja, um inteiro negativo) se nome for diferente e/ou menor que outro_nome;
- result será 0 se, e somente se, nome for igual a outro_nome (entenda por igual o mesmo número de caracteres, e os mesmo caracteres (um 'A' é diferente de um 'a') na mesma posição relativa (índice) em cada vetor;
- result será maior que 0 (ou seja, um inteiro positivo) se nome for diferente e/ou maior que outro_nome.

Em sua forma mais comum, só é verificado o resultado diferente de ou igual a 0.

Copiando uma string em outra (strcpy)
Para copiar o conteúdo de um vetor para outro vetor é necessário copiar item a item, e ainda verificar se o tamanho dos 2 vetores é compatível.
Seria possível copiar um vetor de 20 inteiros em um vetor de 10?
No caso das strings, temos uma função para fazer esta cópia: strcpy. Observe o seu uso:

 strcpy(nome, outro_nome);

Aqui estamos copiando o conteúdo de outro_nome em nome. Preste bastante atenção a isto, porque é muito fácil confundir.
Um detalhe importante é que alguns compiladores(acho que a maioria) permite copiar uma string maior em uma menor sem perda de dados. Na verdade a string menor é aumentada. Como isto é possível? Vejamos a próxima função.

Juntando strings (strcat)
Como juntar 2 vetores afim de formar 1 único vetor?
Poderíamos criar um 3o vetor cujo tamanho fosse igual a soma dos 2, e copiar todos os itens de cada um dos 2 vetores.
No caso de strings, temos uma função que faz isto para nós: strcat. Observe o seu uso:

 strcat(nome, outro_nome);

Ela concatena (que palavra estranha rsrs; prefiro junta ou acrescenta) outro_nome no final de nome. O tamanho de nome será a soma do tamanho anterior de nome com o tamanho de outro_nome.


Existem outras funções especializadas para o trabalho com strings. A medida que elas forem necessárias para a solução de algum problema proposto, nós falaremos sobre elas.
Para encerrar, vou deixar um pequeno código para exemplificar as funções que vimos neste post.


#include "stdio.h"
#include "stdlib.h"
#include "string.h"
 
int main(){
 char nome[21], outro_nome[21];
 char igualdade[10], frase[16] = "As strings sao ";
 int tam_nome, tam_outro;
 
 printf("Digite um nome (no maximo 20 caracteres): ");
 gets(nome);
 tam_nome = strlen(nome);
 printf("Digite um outro nome (no maximo 20 caracteres): ");
 gets(outro_nome);
 tam_outro = strlen(outro_nome);
 if (strcmp(nome, outro_nome) == 0)
    strcpy(igualdade,"iguais");
 else
    strcpy(igualdade,"diferentes");
 printf("%s tem %d caracteres e %s tem %d caracteres\n", nome, tam_nome, outro_nome, tam_outro);
 strcat(frase, igualdade);
 printf("%s\n", frase);
 printf("Se juntar %s e %s ", nome, outro_nome);
 strcat(nome, outro_nome);
 printf("o novo tamanho de %s sera %d\n", nome, strlen(nome));
 system("pause");
}


Viu como é fácil?

quinta-feira, 24 de dezembro de 2009

Um vetor especial... Parte I (string/conceitos)

Hoje vamos falar sobre um vetor que o C trata de forma especial: o vetor de caracteres, também chamado de string.

A comunicação entre o homem e a máquina!
Como já aprendemos aqui, um vetor é um agrupamento de variáveis do mesmo tipo. Até agora trabalhamos com vetores de inteiros. Apesar de termos usado vetores de caracteres desde este post.
Você talvez não tenha percebido ainda, mas tudo o que está digitado aqui são vetores de caracteres. Uma palavra é formada de letras (caracteres) agrupadas (vetor).
Este é o tipo de vetor mais utilizado em programas que interagem com o usuário (mesmo nos programas com interface gráfica).
Por ser muito usado, este vetor é tratado de forma especial pelo C. Não que ele seja mais complicado, mas sim para facilitar o trabalho dos programadores (no caso, nós).
Vejamos como isto é feito.

Não acaba onde termina!
Como já falamos, um vetor em C sempre começa com o índice 0 (zero), mas não possui nenhum restrição quanto ao seu tamanho, ficando sob a responsabilidade do programador.
Relembrando: Se declararmos um vetor de inteiros com o tamanho 5 (int vet[5]), o seu último elemento será o que tem o índice 4 (tamanho do vetor - 1), mas se fizermos uma atribuição usando o índice 5 (vet[5] = 10), ela será aceita (apesar de estar errada). O compilador não tem como saber onde termina este vetor.
No caso do vetor de caracteres é necessário indicar este fim para que o compilador saiba onde ele acaba. Isto é feito com um caracter especial (\0).
Observe a figura abaixo:



Esta é a forma como o C armazena a palavra CASA. Um vetor de caracteres terminado por \0.
A forma de declarar e inicializar este vetor é:

  char vet[5] = {'C', 'A', 'S', 'A'};

ou como é mais comum:

  char vet[5] = "CASA";

Acredito que esta 2a forma é familiar. É ela que temos usado com a função printf. Observe que a palavra CASA tem 4 letras, mas nosso vetor tem tamanho 5, ou seja, existe um caracter a mais para guardar o \0.
No próximo post veremos como esta forma especial que o C trata os vetores de caracteres facilita nossa vida.

Aproveitando o post, gostaria de desejar a todos um ótimo Natal, com saúde, amor e paz, que são as coisas que realmente importam. Um abraço a todos.

quinta-feira, 17 de dezembro de 2009

Vetores x Funções

Recebi um comentário (neste post) com uma pergunta. Gostaria de agradecer o comentário e incentivar outras pessoas a fazer o mesmo. TEM ALGUMA DÚVIDA SOBRE O QUE ASSUNTO DO POST: PERGUNTE NOS COMENTÁRIOS!
Como o objetivo do blog é tornar o C mais fácil, sempre que receber alguma dúvida, responderei, no próprio comentário, quando for simples, ou num novo post, quando esta resposta necessitar de uma atenção maior (como é o caso hoje). Claro que observando a sequência dos assuntos (não venha me perguntar sobre árvores balenceadas ainda, na hora certa vou falar sobre elas).
A pergunta é esta: "Como faço para criar uma função que preencha e some vetores?
Ou não tem como fazer isso com função?"
A resposta é SIM. Antes de mostrar como, vamos ver (e rever) um simples detalhe.

No post em que falamos sobre ponteiros, aqui, vimos que o nome de um vetor corresponde ao seu endereço. Isto significa que um vetor sempre estará acessível a qualquer parte do programa (se conheço o endereço, posso acessar), bastando saber o seu nome. Assim, um vetor SEMPRE é passado às funções por referência, não tendo, nem ao menos, a necessidade de usar o & antes do nome do vetor.
Visto isto, vamos ao como.


//SOMANDO VETORES USANDO FUNÇÕES
#include "stdio.h"
#include "stdlib.h"
 
const int TAM=5;
 
int ler_int(){
 int digitado;
 
 printf("Digite um valor inteiro: ");
 scanf("%d",&digitado);
 return digitado;
}
 
void mostra_int(int num){
 printf("%d ",num);
}
 
void preenche_vetor(int vet[]){
 int i;
 
 for(i = 0; i < TAM; i++)
    vet[i] = ler_int();
}
 
void soma_vetores(int vet1[], int vet2[], int vet3[]){
 int i;
 
 for(i = 0; i < TAM; i++)
    vet3[i] = vet1[i] + vet2[i];
}
 
void exibe_vetor(int vet[]){
 int i;
 
 printf("\n");
 for(i = 0; i < TAM; i++)
    mostra_int(vet[i]);
 printf("\n\n");
}
 
int main(){
 int vetor_A[TAM], vetor_B[TAM], vetor_C[TAM];
 
 printf("Preenchendo o 1o vetor\n");
 preenche_vetor(vetor_A);
 exibe_vetor(vetor_A);
 printf("Preenchendo o 2o vetor\n");
 preenche_vetor(vetor_B);
 exibe_vetor(vetor_B);
 printf("Somando os vetores...\n");
 soma_vetores(vetor_A, vetor_B, vetor_C);
 printf("\n\n");
 printf("Exibindo o vetor resultante da soma\n");
 exibe_vetor(vetor_C);
 system("pause");
}


O código acima soma o conteúdo de 2 vetores (vetor_A e vetor_B) num 3o vetor (vetor_C).
Na linha 5 definimos e inicializamos uma constante inteira TAM que armazena o tamanho dos nossos vetores. Não é obrigatório, mas é uma excelente prática de programação. Por quê?
Neste exemplo trabalhamos com vetores de 5 elementos. Se decidirmos aumentar o tamanho desses vetores para 10 elementos, só precisaremos alterar uma única linha de código. Se não tivéssemos usado a constante precisaríamos alterar 4.
Uma novidade está na linha 19, é a forma de especificar um vetor como um parâmetro de uma função. A sintaxe formal é esta:

 <tipo de retorno> nome da função (<tipo de dados do vetor> nome do vetor [])

Note que não é necessário informar o tamanho do vetor.
Poderíamos inclusive usar a seguinte sintaxe (que é muito comum em programas profissionais):

 <tipo de retorno> nome da função (<tipo de dados do vetor>* nome do vetor)

O nome de um vetor corresponde ao seu endereço na memória, portanto, podemos tratá-lo como um ponteiro. Pode fazer um teste se quiser.
Já quando chamamos a função, linha 46, não é necessário o uso dos [].
Um erro muito comum é colocar os [] com o tamanho do vetor dentro deles. Ao fazer isto, ao invés de se passar o vetor inteiro, está sendo passada à função um único elemento do vetor, e pior ainda, um elemento que nem faz parte do vetor, já que por iniciar em 0(zero), o último elemento de um vetor é igual a tamanho do vetor - 1.
O restante de código já é (acredito ;)conhecido.

Mais alguma dúvida?

(Para pensar: Uma função pode retornar um vetor?)

terça-feira, 15 de dezembro de 2009

Dividir para conquistar! Final (valor x referência)

Encerrando a nossa série sobre modularidade (existem outras coisas a falar, mas elas merecem atenção especial, ou seja, um post só para elas), vamos ver com mais detalhes como as variáveis 'caminham' entre as funções.

Como vimos no post anterior desta série (aqui), as variáveis declaradas entre as {} de uma função são locais à função, ou seja, não são acessíveis por outras funções do programa. No entanto, vimos, aqui, que é possível passar o retorno de uma função, que geralmente é uma variável local da função, para outras funções. Como isso acontece?

Não te conheço, mas sei o que você tem!
A forma correta de chamar uma função é passando os argumentos para os parâmetros que ela utiliza para fazer o seu trabalho. No exemplo que vimos na 1a parte desta série, para somar dois números, na linha 25 passamos as variáveis num1 e num2 como argumentos para a função soma_int que utiliza as variáveis val1 e val2 para fazer o seu trabalho.
Na verdade não passamos as variáveis, e sim o seu conteúdo. O que acontece realmente é que o conteúdo da variável num1 é copiado para a variável val1, e o conteúdo da variável num2 é copiado para a variável val2.
Esta operação é chamada de passagem por valor, porque os valores armazenados nos argumentos são copiados para os parâmetros.
Uma vez que a função trabalha com uma cópia do conteúdo das variáveis num1 e num2, o seu conteúdo permanece intacto, e a função soma_int consegue fazer o seu trabalho corretamente (todos ficam satisfeitos rsrs).
Mas, e se precisarmos alterar o conteúdo das variáveis num1 e num2? Seria possível?
Vejamos isto na prática. Observe o código abaixo.


//TROCANDO O CONTEÚDO DE VARIÁVEIS USANDO FUNÇÕES
#include <stdio.h>
#include <stdlib.h>
 
int ler_int(){
 int num;
 
 printf("Digite um numero inteiro: ");
 scanf("%d", &num);
 return num;
}
 
void troca(int val1, int val2){
 int aux;
 
 aux = val1;
 val1 = val2;
 val2 = aux;
}
 
void exibe_int(int val1, int val2){
 printf("\nO valor de num1 = %d e num2 = %d.\n", val1, val2);
}
 
int main(){
 int num1, num2;
 
 num1 = ler_int();
 num2 = ler_int();
 exibe_int(num1, num2);
 troca(num1, num2);
 exibe_int(num1, num2);
 system("pause");
}


Antes de executar este código, vamos falar sobre a função troca, que aparece na linha 13. Na linha 14, declaramos uma variável aux, que usaremos para salvar o valor armazenado na variável val1 antes de armazenar nela o valor da outra variável, val2, como mostrado nas linhas 16 e 17. O valor salvo em aux, ou seja o valor anterior de val1, será agora armazenado em val2.
Agora que entendemos como funciona a função troca, pode executar o código e observar o resultado.
Acredito que você já sabe o porquê dele não funcionar. Se não sabe, veja a razão.
Quando a função troca é chamada na linha 31, dentro da main, ela recebe os valores das variáveis num1 e num2, que são copiados para as variáveis val1 e val2 que são os seus parâmetros. Na verdade, os valores que são trocados são os de val1 e val2, e não os valores de num1 e num2 como desejado.
Para resolver situações como estas, entra em ação um recurso que já conhecemos: os ponteiros.

Não adianta se esconder... Eu sei onde você mora!
Se você não se recorda do funcionamento de um ponteiro, dê uma olhada aqui.
Agora observe como fica o nosso código usando este recurso.


//TROCANDO O CONTEÚDO DE VARIÁVEIS USANDO FUNÇÕES
#include <stdio.h>
#include <stdlib.h>
 
int ler_int(){
 int num;
 
 printf("Digite um numero inteiro: ");
 scanf("%d", &num);
 return num;
}
 
void troca(int* val1, int* val2){
 int aux;
 
 aux = *val1;
 *val1 = *val2;
 *val2 = aux;
}
 
void exibe_int(int val1, int val2){
 printf("\nO valor de num1 = %d e num2 = %d.\n", val1, val2);
}
 
int main(){
 int num1, num2;
 
 num1 = ler_int();
 num2 = ler_int();
 exibe_int(num1, num2);
 troca(&num1, &num2);
 exibe_int(num1, num2);
 system("pause");
}


Agora funcionou! Você sabe me dizer porquê?
Este uso de ponteiros para tornar possível o acesso a variáveis locais de outras funções é chamado de passagem por referência. Porque na verdade, não se passa o valor da variável, mas a sua referência, ou seja, o seu endereço na memória, que como sabemos, torna possível acessar e alterar esta variável.
Viu como é fácil (os ponteiros são nossos amigos rsrs).
Até a próxima.

sábado, 12 de dezembro de 2009

Dividir para conquistar! Parte II (local x global)

Continuando o que começamos aqui, hoje, como prometido, vamos falar sobre o 'comportamento' das variáveis quando usamos modularidade.

Vamos brincar de esconde-esconde?
Existe um conceito em C chamado de escopo. Já procurei uma definição literal para a esta palavra e não encontrei. Então como costumo fazer, vamos à pratica.


#include "stdio.h"
#include "stdlib.h"
 
int b = 5;
 
int main(){
 int a, c;
 
 a = 6;
 c = a + b;
 printf("\na = %d, b = %d e c = %d.\n",a,b,c);
 {
    int a, c;
 
    a = 15;
    c = a + b;
    printf("\na = %d, b = %d e c = %d.\n",a,b,c);
    b = c;
 }
 c = a + b;
 b = 2 * a;
 printf("\na = %d, b = %d e c = %d.\n",a,b,c);
 system("pause");
}


Observe o código acima. Aliás, não só observe, fique curioso e coloque ele para rodar no seu micro. Já colocou? Viu a saída?
Vamos entender.
A princípio só vemos declarações de variáveis e atribuições. Mas na linha 12 vemos algo diferente (que acredito não ser comum em códigos fonte profissionais, mas funciona rsrs). Como sabemos, as {} separam um bloco de comandos, geralmente estes blocos estão ligados a instruções de decisão e repetição (if, while, for), e também a funções; mas elas também funcionam sozinhas, como mostrado neste código.
Observe os comandos das linhas 7 e 13, e das linhas 10 e 16. São os mesmos comandos, só as variáveis envolvidas que são diferentes, apesar de terem os mesmos nomes.
Como já vimos, aqui, os nomes das variáveis são 'etiquetas' (rótulos) que identificam endereços de memória. Apesar de estarem usando a mesma 'etiqueta', os endereços das variáveis declaradas nas linhas 7 e 13 são diferentes. O C não se confunde por causa das {}.
Quando são feitas as operações dentro das {}, são usadas as variáveis declaradas dentro das {}. Assim o valor da variável a que é impresso no printf da linha 11 é um, e o da linha 17 outro. O valor de a impresso pelo printf na linha 21 é o mesmo impresso pelo printf da linha 11, pois é a mesma variável. A atribuição da linha 15 não altera o seu valor, já que ela altera outra variável, que por acaso (neste caso, propositalmente) tem o mesmo nome. Dizemos que as duas variáveis tem escopo diferente.
Quando se trata de funções, dizemos que a variável declarada entre as {} do bloco de comandos de uma função é uma variável local da função. Ela fica 'escondida' dentro das {}, e para todas as outras funções é como se ela nem existisse.
Lembra do que já falamos sobre funções. Uma função é uma 'caixa preta', não precisamos saber como ela faz as coisas, apenas os parâmetros que ela precisa e o retorno que fornece (isto quando ela precisa de parâmetros e dá algum retorno). Ela pode usar uma série de variáveis, mas não precisamos tomar conhecimento da existência delas.
Em oposição a isto, existe um tipo de variável chamada de global. Temos um exemplo de uma variável assim na linha 4 do nosso código exemplo acima: é a variável b.
Ela é declarada e inicializada fora de qualquer bloco, fora até mesmo da função main. Observe que utilizamos o valor de b para fazer a operação de soma da linha 10, e usamos este mesmo valor na soma da linha 16, que está dentro das {}.
Por ser global, esta variável é 'vista' por qualquer parte do programa (funções, inclusive), e também pode ser alterada em qualquer parte do programa.
Fazemos uma alteraçao no valor da variável b na linha 18, que está dentro das {}, e este novo valor de b é usado na soma que é realizada na linha 20, fora das {}, e depois alterado na linha 21.
Até o próximo post.

domingo, 6 de dezembro de 2009

Dividir para conquistar! Parte I (funções)

Até o presente momento vimos exemplos de códigos fontes pequenos, com uma utilidade muito pequena (coisa muito difícil de encontrar no 'mundo real'), e portanto, muito fácil de acompanhar, compreender o seu funcionamento e eliminar os seus erros.
Eu já disse algumas vezes aqui no blog que é fácil encontrar códigos fonte com mais de 1000 linhas. Imagine a dificuldade para acompanhar um código desses!
E imagine a quantidade de tempo que 1 programador levará para concluir este código!
Para resolver estas questões o C usa uma metodologia chamada de modularidade.

O que é modularidade?
Imagine que você fosse o chefe de um grupo que deveria criar uma maquete de uma fábrica. Lógico que você iria dividir este trabalho de criação em várias partes. Dividindo entre os componentes do grupo cada uma dessas partes de acordo com o que cada um sabe fazer. Não seria inteligente deixar alguém fazer algo que não é sua especialidade tendo na equipe um especialista nesta tarefa.
Isto é modularidade!
Dividir uma tarefa grande em várias partes menores e especializadas. Quando digo especializada, quero dizer que idealmente, cada módulo fará uma única tarefa.(O que você prefere? Ser atendido por um médico especializado na sua doença ou por um clínico geral?)
Cada um desses módulos é chamado de função.
As funções podem ser classificadas de acordo com seus parâmetros e retorno.

O que são parâmetros? E retorno?
Algumas funções precisam de dados para cumprir a tarefa para a qual foram criadas. Esses dados são chamados de parâmetros da função.
Voltando ao exemplo da maquete, se você quiser um prédio em sua maquete, terá que dizer a quem irá fazê-lo quantos andares ele terá, qual a cor dele, se terá entrada lateral, etc. Se essas informações não forem passadas, o responsável não saberá como fazê-lo, ou poderá fazê-lo da forma que ele quiser, que pode não ser a que você desejava.
De posse dessas informações (argumentos), o prédio será construído conforme especificado, seguindo esses parâmetros.
Existem funções que precisam de vários parâmetros, outras que precisam de apenas 1 parâmetro, e ainda, as que não precisam de nenhum paramêtro.
Se você quisesse a sua maquete bem realista, poderia pedir a um componente do grupo que fizesse alguns carros populares para ficar no estacionamento da fábrica. Este componente é especialista em fazer carros populares, portanto, não é necessário passar nenhuma informação para ele. Ele fará os carros e entregará.
Já o retorno é o dado que a função devolve depois de fazer o seu trabalho.
No exemplo da maquete, seria o prédio e os carros.
Diferente dos parâmetros, o retorno só permite duas configurações: 1 ou nenhum.
Você pode se perguntar: Mas não seriam vários carros?
Você está certo. O que acontece na verdade, é que o responsável por fazer os carros, os faz um a um. E também os entrega um a um.
Neste exemplo da maquete, você é o chefe da equipe, o coordenador do projeto. Você passa a tarefa para o componente, e ele devolve o trabalho pronto. Você apenas agrupa o resultado. Pode ser que inclusive, você tenha que passar o resultado final do trabalho de um componente para um outro componente como a matéria-prima deste.
Num programa em C, esta função de coordenação é feita pela função main (sim, a main é uma função!).
Vamos ao exemplo de um programa que faz a soma de 2 números inteiros digitados pelo usuário usando este conceito de modularidade.
Antes de ver o código propriamente dito, vamos pensar nas funções que precisamos para executar essa tarefa.
1o precisamos de uma função que receba os valores inteiros digitados pelo usuário e devolva estes valores. Como sabemos ela não pode devolver os 2 valores de uma só vez. Então ela deve ler um valor e retornar este único valor. Depois ela será executada de novo para o outro valor.
Esta função precisa de algum parâmetro? A resposta é NÃO. Ela é especializada em ler inteiros, e inteiros são sempre inteiros. Acredito que ela não necessite de nenhum. E tem algum retorno? Com certeza. São os valores digitados pelo usuário.

2o precisamos de uma função que faça a soma propriamente dita.
Esta função precisa de parâmetros? Com certeza. São os valores digitados pelo usuário, que vieram da função anterior.
E tem algum retorno? Com certeza. É o resultado da soma.

3o precisamos de uma função que mostre ao usuário o resultado da soma.
Esta função precisa de algum parâmetro? Com certeza. É o resultado da soma retornado pela função anterior.
E tem algum retorno? A resposta é NÃO. Já que o valor vai ser mostrado e nada mais será feito no programa, não existe esta necessidade.

Vamos ver uma representação gráfica disto:



As setas indicam 'o caminho' das variáveis. As vermelhas indicam as variáveis que as funções retornam para a função main, e as pretas indicam as variáveis que a função main passa para as funções. (Eu iria usar outras cores, mas não poderia deixar de fazer esta homenagem ao HEXA DO MENGÃO! rsrs)

Uma função em C, é declarada da seguinte forma:

 <tipo de retorno> nome da função (<tipo do parâmetro> nome do parâmetro, <tipo do parâmetro> nome do parâmetro, ..., <tipo do parâmetro> nome do parâmetro){
    comandos da função
}


Vamos ao código, que eu explico os detalhes depois.


//EXEMPLO DO USO DE FUNÇÕES
#include <stdio.h>
#include <stdlib.h>
 
int ler_int(){
 int num1;
 printf("Digite um numero inteiro: ");
 scanf("%d", &num1);
 return num1;
}
 
int soma_int(int val1, int val2){
 return val1 + val2;
}
 
void exibe_result(int resultado){
 printf("O resultado da operacao eh %d.\n", resultado);
}
 
int main(){
 int num1, num2, result;
 
 num1 = ler_int();
 num2 = ler_int();
 result = soma_int(num1, num2);
 exibe_result(result);
 system("pause");
}


A 1a função, que se encontra na linha 5, é a ler_int. Pelo seu cabeçalho, vemos que é uma função que devolve um valor inteiro e não necessita de nenhum parâmetro. Vejamos o que esta função faz.
Na linha 6 é declarada uma variável inteira num1, que será usada para receber o valor digitado pelo usuário e retornar este valor para a função main.
Observe a linha 7 onde é mostrada ao usuário uma mensagem indicando o que ele deve fazer. Poderíamos ter algum código para verificar se o valor digitado está dentro de uma faixa específica, dentre outras coisas.
Na linha 8, o valor digitado pelo usuário é armazenado na variável inteira num1 usando o scanf. Até aqui não há nada que já não vimos antes.
A novidade está na linha 9. Observe o uso da palavra reservada return. É assim que é feito o retorno (será por isso que ele tem esse nome?) do valor para a função main. Neste caso, o valor retornado será o que está armazenado na variável inteira num1.
Na linha 12, temos a nossa 2a função, que é a soma_int. Esta função retorna um valor inteiro e necessita de 2 parâmetros inteiros (não é obrigatório que os parâmetros sejam do mesmo tipo retornado pela função, poderia ser de qualquer tipo e em qualquer quantidade, como já vimos).
Um outro detalhe interessante está na linha 13. Observe que é retornado o resultado da soma dos valores passados como parâmetros. Poderíamos ter declarado uma variável inteira, ter atribuído o resultado desta soma à variável e, só então, retornar o valor armazenado nela. A escolha de uma forma ou outra é exclusiva do programador. Eu, particularmente, prefiro como se encontra no código.
A 3a função, exibe_result, que aparece na linha 16, já começa com uma novidade.
Como vimos acima, esta função não retorna nenhum valor. A forma em C de especificar isto é usando o tipo void (lembra dele?). Isto significa que a função não retorna NADA. Além disto, ela necessita de 1 parâmetro, que é o que vai ser exibido, neste caso o resultado da soma que é retornado pela função soma_int.
A última função deste código é a main, que aparece na linha 20. Como já falei antes é ela que coordena o 'trabalho' das outras funções. Vamos ver como ela faz isto.
Na linha 21 são declaradas 3 variáveis inteiras. Elas são necessárias para receber o valor retornado pelas funções (se uma função devolve um valor, ele tem que ser armazenado em algum lugar ou será perdido).
Observe que uma destas variável tem o nome de num1, o mesmo nome daquela variável usada pela função ler_int, lá na linha 6. Antes que você reclame, deixe-me falar uma coisa: elas tem o mesmo nome, mas são a mesma 'pessoa' (rsrs).
A variável num1 de ler_int, só 'existe' enquanto a função ler_int está em execução. E enquanto isto a variável num1 da main, fica 'desativada'.
Dizemos que essas variáveis são locais. Mais a frente falaremos detalhadamente sobre isto. Por enquanto basta saber isto.
Na linha 23, está a 1a chamada da função ler_int. Este é o nome técnico usando para indicar que uma função será executada. Observe que é feita uma atribuição nesta mesma linha. Como já falamos, é para salvar o retorno da função.
Quando não existe um retorno, a forma da chamada é igual a da linha 26: somente o nome da função e os argumentos (os valores que são passados para preencher os parâmetros de uma função são chamados de argumentos) que ela necessita, se ela necessitar de algum.
Acredito que o restante do código agora ficou fácil de entender.
Uma outra coisa interessante a respeito das funções, é que é possível passar o retorno de uma função diretamente como argumento para outra função, sem precisar armazená-lo em uma variável.
Olhe este código abaixo e veja se consegue entender o que ele faz.


//EXEMPLO DO USO DE FUNÇÕES
#include <stdio.h>
#include <stdlib.h>
 
int ler_int(){
 int num1;
 printf("Digite um numero inteiro: ");
 scanf("%d", &num1);
 return num1;
}
 
int soma_int(int val1, int val2){
 return val1 + val2;
}
 
void exibe_result(int resultado){
 printf("O resultado da operacao eh %d.\n", resultado);
}
 
int main(){
 
 exibe_result(soma_int(ler_int(), ler_int()));
 system("pause");
}


Fácil não é?
Até a próxima.

quarta-feira, 2 de dezembro de 2009

Tá apontando o quê? (ponteiros)

Além dos tipos básicos e dos tipos estruturados (que vimos aqui e comentamos alguma coisa aqui), o C possui um tipo de dados especial: o tipo ponteiro.

A função principal do ponteiro é ... APONTAR (não é brincadeira!).
Vamos ver como isso funciona.

Eu sei onde você está!
Podemos imaginar um ponteiro como uma posição na memória onde está armazenado algum valor que pode ser alterado. Então um ponteiro é uma variável?

SIM e NÃO (rsrs).

A diferença entre um ponteiro e uma variável está no tipo de valores que eles podem armazenar. Enquanto uma variável pode armazenar caracteres, números inteiros e reais; um ponteiro armazena endereços de memória.
No post em que falamos sobre vetores (aqui), vimos a relação entre as variáveis e a sua posição na memória. Observe a figura abaixo (que é a última daquele post).



Se você clicou no link e viu o post sobre vetores, irá reparar que a numeração abaixo dos 'quadrinhos' está diferente. Por quê?

É diferente... mas é a mesma coisa!
Enquanto no referido post os números abaixo dos 'quadrinhos' indicavam os índices do vetor, neste se referem aos endereços de memória.
Um ponteiro que apontasse para o 3o elemento deste vetor estaria armazenando o valor 52.
Repare que os valores de endereço aumentam (ou diminuem) de 4 em 4. Isto acontece porque, neste exemplo, são necessários 4 bytes para armazenar um valor inteiro. Portanto, o ponteiro de que falamos acima é um ponteiro para inteiros. Se fosse um vetor de caracteres, onde cada caracter é armazenado em 1 byte, precisaríamos de um ponteiro para caracteres.

E qual é a vantagem de usar um ponteiro?
Antes de ver um ponteiro em ação (rsrs), vamos ver a sua sintaxe.

 int *pint;

O comando acima declara uma variável do tipo ponteiro para inteiros com o nome pint. Se quisermos apontar para um float o comando seria:

 float* pfl

Não é necessário que o nome do ponteiro comece com p (embora seja uma boa prática), nem que tenha o tipo da variável. Só deve ter o tipo e o *, que pode estar tanto junto ao nome como ao tipo.

Uma das vantagens no uso de um ponteiro está no fato de que além de apontar para o endereço de uma variável, também é possível ter acesso ao conteúdo desta variável usando o ponteiro. A esta operação se dá o nome de dereferenciar um ponteiro.

Como sempre, vamos ver isso na prática.


//EXEMPLO DO USO DE UM PONTEIRO
#include <stdio.h>
#include <stdlib.h>
 
int main(){
 int* pnum;
 int num = 10;
 
 pnum = &num;
 printf("O valor inicial de num eh %d.\n",*pnum);
 *pnum += 2;
 printf("O valor atual de num eh %d.\n",num);
 num--;
 printf("O valor final de num eh %d.\n",*pnum);
 system("pause");
}


O código acima é muito simples, mas mostra alguns fundamentos muito importantes. Vamos entender o que ele faz.
Na linha 6 definimos uma variável ponteiro para inteiros chamada de pnum. Na linha 7 definimos uma variável inteira num e a inicializamos com o valor 10.
Um ponteiro armazena endereços de memória. E é isso que fazemos na linha 9. Atribuimos o endereço da variável inteira num ao ponteiro para inteiros pnum.
Observe que a variável é do mesmo tipo do ponteiro. Se não fosse esta atribuição seria NÃO poderia ser feita.
Na linha 10 exibimos o valor de num dereferenciando o ponteiro pnum.
Além de acesso ao conteúdo da variável, também podemos alterá-lo usando o ponteiro, o que é mostrado na linha 11 e comprovado na linha 12. Acredito que agora você já saiba o que fazem as outras linhas (rsrs).
Você deve estar se perguntando o por quê de usar um ponteiro ao invés da própria variável. Isto vai se tornar claro quando falarmos sobre modularidade, que é a possibilidade de dividir um programa em várias partes, sendo que as variáveis de cada parte estão 'escondidas' das outras partes.

Outro uso muito comum de ponteiros é como uma forma alternativa de acessar elementos de um vetor. Esse acesso é feito através da aritmética de ponteiros.

Vetor x Ponteiro
Os dados armazenados numa variável do tipo ponteiro são endereços de memória (quantas vezes eu já repeti isso???). Esses endereços são sempre valores numéricos. E com esses valores podemos fazer algumas operações aritméticas (SOMENTE SOMA E SUBTRAÇÃO).
Observe este dois comandos aplicados a um ponteiro para inteiro pnum:

 (*pnum)++;
 pnum++;

O 1o comando incrementa o conteúdo da posição de memória apontada por pnum, ou seja, se na posição apontada por pnum estiver um inteiro com o valor 11, após este comando o valor deste inteiro passa a ser 12.
Já o 2o comando incrementa o valor armazenado em pnum. Como pnum é um ponteiro para inteiros, que estamos supondo necessitarem de 4 bytes para serem armazenados na memória, ao valor de pnum será somado 4.
Por exemplo, digamos que no código exemplo que usamos acima o endereço da variável inteira num fosse 38. Este valor foi atribuido ao ponteiro para inteiros pnum. Então o conteúdo de pnum é 38. Ao executar este 2o comando este conteúdo passa a ser 42. Ou seja, pnum não estará mais apontando para num, e passará a apontar para uma posição de memória na qual não sabemos o que está armazenado. Isto é extremamente perigoso, pois torna possível alterar algo que não deveria ser alterado.
Se para uma única variável isto é perigoso, para um vetor é um ótimo recurso, afinal um vetor é um grupo de variáveis do mesmo tipo armazenadas em sequência na memória.
Observe como ficaria o 2o exemplo do post sobre vetores utilizando aritmética de ponteiros.


//PROGRAMA QUE RECEBE 100 NÚMEROS INTEIROS
#include <stdio.h>
#include <stdlib.h>
 
int main(){
 int vetor[100], i;
 int* pvet;
 
 pvet = vetor;
 printf("Digite 100 numeros inteiros:\n");
 for(i=0; i < 100; i++){
    printf ("Valor %d: ",i+1);
    scanf("%d", pvet++);
 }
 system("pause");
}


Na linha 6 declaramos um vetor de 100 inteiros e uma variável inteira i que será usada para dar ao usuário uma noção de quantos valores ele já digitou. Na linha 7 definimos uma variável ponteiro para inteiros chamada pvet. Na linha 9 atribuimos o endereço do vetor ao ponteiro pvet (Numa atribuição, o nome de um vetor corresponde ao seu endereço). Na linha 13, dentro do for, é feita a leitura do valor digitado pelo usuário para a posição de memória apontada por pvet, que da 1a vez é a 1a posição do vetor, e depois o conteúdo de pvet é incrementado, passando a apontar para a próxima posição do vetor.

Viu como não é tão difícil quanto parece?