Mostrando postagens com marcador vetor. Mostrar todas as postagens
Mostrando postagens com marcador vetor. Mostrar todas as postagens

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á

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?)

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?

segunda-feira, 30 de novembro de 2009

Juntos somos mais eficientes! (vetores)

Até agora vimos somente os tipos básicos do C (aqui), e fizemos muita coisa com eles. Mas chega um momento em que eles não são eficientes para resolver alguns problemas. Quer um exemplo?
Imagine um programa que receba 100 valores inteiros digitados por um usuário. Imaginou? Se não, veja como ele seria usando o que vimos até agora.


//PROGRAMA QUE RECEBE 100 NÚMEROS INTEIROS
#include <stdio.h>
#include <stdlib.h>
 
int main(){
 int val001, val002, val003, val004, val005, ... , val100;
 
 printf("Digite 100 numeros inteiros:\n");
 scanf("%d",&val001);
 scanf("%d",&val002);
 scanf("%d",&val003);
 scanf("%d",&val004);
 scanf("%d",&val005);
    .
    .
    .
 scanf("%d",&val100);
 system("pause");
}


Percebeu a quantidade de linhas que esse programa teria? Sabia que é possível fazer este mesmo programa com 16 linhas (até menos) e de forma muito mais inteligente?
Dê uma olhada nesse exemplo.


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


Este código acima está completo e funciona perfeitamente (não faz muita coisa, é verdade, mas funciona ;). Vamos ver o 'segredo' por trás disto.

Observe a linha 6. Nela é declarado um vetor de 100 inteiros. Vetor???

Um grupo de iguais.

Um vetor é um tipo estruturado homogêneo.(???)
Estruturado significa que ele é uma composição de tipos básicos, ou seja, tipos básicos organizados em um grupo/agrupamento. E homogêneo, significa que esses tipos básicos são iguais; no nosso exemplo, são 100 inteiros (é possível criar agrupamentos heterogêneos, como veremos em outro momento).
Não entendeu ainda? Quer que eu desenhe?



A figura acima representa um vetor de 4 inteiros armazenado na memória. Observe que o armazenamento é consecutivo, ou seja, cada valor inteiro está armazenado num endereço da memória, e estes endereços estão em sequência. Se tivéssemos declarado 4 variáveis inteiras não poderíamos garantir que elas ficariam armazenadas em sequência na memória.
Por estarem armazenados em sequência na memória, podemos acessar o conteúdo de cada endereço também em sequência. Esse é o 'truque' do vetor.
Mas como é feito esse acesso?

Leia o índice!

Com certeza você já teve contato com algum livro (se não, aconselho que o faça rsrs). Podemos generalizar um livro como uma sequência de páginas (escritas ou não) agrupadas. Todas as páginas tem o mesmo tamanho (não me lembro de ter visto algum em que isto não fosse verdadeiro).
Quando queremos encontrar alguma informação específica num livro temos duas opções: ou folheamos o livro, página por página, analisando uma a uma; ou vamos ao seu índice e localizamos o número exato da página onde está a informação que procuramos. Qual delas você considera mais eficiente?
Em termos de comparação, tirando as muitas diferenças, um vetor se assemelha muito a um livro. São posições de memória do mesmo tamanho agrupadas contendo algum valor válido (ou não). Já que são tão parecidos, por que não usar um índice também com um vetor? Facilitaria muito, não concorda? Pois é exatamente assim que funciona.
Vamos a outro desenho.



Para acessar o conteúdo da 1a posição do vetor da figura acima, é só se referir ao nome do vetor e ao índice 0 (zero). Os vetores em C SEMPRE começam com o índice 0 (zero).
Digamos que o nome do vetor da figura acima seja vet. Para exibir o conteúdo da sua 1a posiçao, usamos o comando:

 printf("%d", vet[0]);

Seria impresso o valor 67.

Fácil, não? Só devemos prestar atenção a 2 pontos importantíssimos:

1o. O primeiro índice de um vetor em C SEMPRE começa em 0 (zero)
2o. O último índice de um vetor em C é SEMPRE o tamanho do vetor menos 1.

Isso não significa que não se possa acessar um elemento fora do vetor.
Se usássemos no vetor dos desenhos acima o comando:

 printf("%d",vet[4]);

Algo seria impresso na tela. Não temos como saber o que seria, mas que funcionaria, funcionaria. Por quê?

Um nome de um vetor seguido de um índice i significa que queremos acessar o endereço que está i posições depois do endereço inicial do vetor.
Se um vetor de 4 inteiros está armazenado na memória, por exemplo, a partir do endereço 44 e o tamanho de um inteiro é 4 bytes; o elemento de índice 3 deste vetor estará no endereço 56.

 endereço inicial do vetor(44) + 3 * (tamanho do tipo do vetor (4)

Se tentarmos acessar o índice 4, que sabemos 'não existir', na verdade estaremos acessando o endereço 60 da memória, e 'interpretando' o seu conteúdo como um valor inteiro (este conteúdo pode ser qualquer coisa que esteja na memória, possivelmente deixada por algum outro programa, frequentemente no referimos a ele como 'lixo').
Algumas linguagens de programação não permitem esse tipo de acesso, mas o C não faz restrições. É sua responsabilidade como programador evitar esse tipo de coisa.

Agora que conhecemos o tipo vetor, ficou fácil entender o código do nosso exemplo. Certo?