Ponteiros e Arrays

No primeiro post desta série, falamos um pouco sobre Ponteiros. No segundo, falamos de Referências. Hoje abordaremos a relação íntima (ui!) entre ponteiros e arrays (ou matrizes).

Relembrando Arrays

Um array, ou matriz, ou ainda um arranjo, é uma abstração matemática utilizada para representar um conjunto de dados homogêneos, ou seja do mesmo tipo (int, float, etc). Essa abstração é organizada em formato de tabela, com linhas e colunas. Cada elemento na matriz possui coordenadas únicas (linha e coluna), de forma que um dado elemento E(i,j) representa o único elemento na posição "linha i", "coluna j".

A sintaxe de declaração de matrizes segue abaixo:

// Matrizes unidimensionais ou vetores
int     ivet[10];               // 10 elementos, do 0 ao 9
char    cvet[23];               // 23 elementos, do 0 ao 22

// Matrizes bidimensionais
int     imat[2][3];             // 2 linhas (0 a 1) e 3 colunas (0 a 2)
double  dmat[10][2];            // 10 linhas (0 a 9) e 2 colunas (0 a 1)

Cada elemento da matriz é independente dos demais e pode ser acessado conforme a sintaxe abaixo:

int ivet[10];
int imat[3][4];

// Alterando o quarto elemento do vetor ivet.
// Lembre-se que começa-se a contar a partir do elemento zero
ivet[3] = 13;

// Lendo o segundo elemento do vetor ivet
int num = ivet[1];

// Alterando o elemento da primeira linha, segunda coluna de imat
imat[0][1] = 42;

// Lendo o elemento na terceira linha, quarta coluna de imat
int foo = imat[2][3];

Neste post não vamos discutir a fundo matrizes, vamos apenas investigar a relação entre matrizes e ponteiros de uma forma bastante intuitiva.

Tamanhos de matrizes

Dado que uma matriz é uma abstração que comporta vários valores de um mesmo tipo, qual o tamanho dela? Quanto espaço ele ocupa na memória?

Considere o código abaixo:

int     ivet[10];
char    cvet[13];
double  dvet[20];

char    cmat[3][4];
int     imat[5][4];

cout << "sizeof(int)     = " << sizeof(int) << endl;
cout << "sizeof(char)    = " << sizeof(char) << endl;
cout << "sizeof(double)  = " << sizeof(double) << endl;

cout << "sizeof(ivet)    = " << sizeof(ivet) << endl;
cout << "sizeof(cvet)    = " << sizeof(cvet) << endl;
cout << "sizeof(dvet)    = " << sizeof(dvet) << endl;
cout << "sizeof(cmat)    = " << sizeof(cmat) << endl;
cout << "sizeof(imat)    = " << sizeof(imat) << endl;

O resultado é bastante razoável. O Espaço ocupado por uma matriz é igual ao número de elementos multiplicado pelo tamanho do tipo do elemento (linhas x colunas x sizeof(tipo)). Ora, se cada elemento é independente, supõe-se que cada um ocupe um lugar separado na memória, caso contrário um elemento sobrescreveria outro. Sendo assim, será que cada elemento possui seu próprio endereço de memória?

Matrizes e endereços de memória

Para facilitar, vamos então analisar os possíveis endereços de uma matriz de caracteres, cujo tamanho de um dado único é 1 byte:

const int max = 5;
char cvet[max] = {'A', 'B', 'C', 'D', 'E'};

// Mostrando o índices, valores e endereços dos dados.
printf("Índice\tValor\tEndereço do elemento\n");
for (int i = 0; i < max; i++) {
    printf("%d\t%c\t%p\n", i, cvet[i], &cvet[i]);
}

// Mostrando o endereço da própria matriz
printf("Endereço da matriz: %p\n", &cvet);

// Mostrando o endereço da própria matriz denovo
printf("Endereço da matriz: %p\n", cvet);

Os endereços dos elementos são sequenciais, ou seja, cada elemento é armazenado ao lado do anterior. Adicionalmente existem dois fatos ainda mais interessantes:

  1. O endereço do array (&cvet), mostrado na linha 25, é o mesmo do primeiro elemento do array;
  2. A própria variável cvet pode ser interpretada como um ponteiro, como é mostrado na linha 28;

Em C++, um array comum é um bloco contíguo de memória cujo nome pode ser interpretado (cast) como um ponteiro que aponta para o seu primeiro elemento. Adicionalmente é válido fazer um ponteiro apontar para um array desde que o ponteiro destino seja para o mesmo tipo que o tipo dos elementos do array. Durante a atrubuição de um array a um ponteiro, o compilador faz uma conversão implícita de tipos. O ponteiro destino passa a ser interpretado como um ponteiro para a área de memória ocupada pelo array.

Uma das consequências não tão óbvias é que durante o cast implícito, é perdida a informação de que aquela área de memória era um array. Portanto é perdida a informação do tamanho do array. Do ponto de vista do ponteiro, ele está apontando para o começo de um bloco arbitrário de memória, de um tamanho também arbitrário. Sair de um array e ir para um ponteiro significa ir de uma abstração mais restritiva e mais alto nível, para um abstração menos restritiva e mais baixo nível.

Por outro lado tentar atribuir um ponteiro a um array gera um erro de compilação por tipos incompatíveis. Um array é um bloco de memória de n dados (bytes), já um ponteiro possui apenas um dado, um endereço. O compilador não tem como saber de antemão se um ponteiro aponta para uma área de 1, 2 ou 200 bytes.

const int max = 300;
char cvet[max];
char* pc = 0;

printf("\nAntes da atribuição\n");
printf("cvet = %p\n", cvet);
printf("pc   = %p\n", pc);
printf("sizeof(cvet) = %lu\n", sizeof(cvet));
printf("sizeof(pc)   = %lu\n", sizeof(pc));

pc = cvet;

printf("\nDepois da atribuição\n");
printf("cvet = %p\n", cvet);
printf("pc   = %p\n", pc);
printf("sizeof(cvet) = %lu\n", sizeof(cvet));
printf("sizeof(pc)   = %lu\n", sizeof(pc));

Note que antes da atribuição (linha 25) o ponteiro pc. está nulo, pois foi inicializado assim. Já os tamanhos indicam que o array possui 300 bytes e o ponteiro apenas 8 (minha máquina é um amd 64). Após a atribuição ambos passam a "apontar" para a mesma área de memória, porém os tamanhos não mudam. Houve um cast implicito de char[300] para char*, e nessa brincadeira o ponteiro pc não tem como saber o tamanho da área de memória para o qual ele aponta. Já o array cvet continua sabendo direitinho o que ele é, sem nenhuma crise existencial.

Aritmética de ponteiros - Vulgo, e daí?

Ora, mas se eu sei que os dados numa matriz estão dispostos lado a lado, eu posso utilizar um ponteiro que vai pulando para o endereço ao lado e acessando o elemento seguinte. O nome disso é aritimética de ponteiros.

const int max = 6; 
char cvet[max] = {'B','L','A','B','O','S'};

char* pc = cvet;

for (int i = 0; i < max; i++) {
    printf("%c", *(pc + i));
}
printf("\n");

for (int i = 0; i < max; i++) {
    printf("%c", *pc++);
}
printf("\n");


// Agora com inteiros
int ivet[max] = {1, 2, 3, 4, 5, 6};
int* pi = ivet;

for (int i = 0; i < max; i++) {
    printf("%p = %d\n", pi, *pi++);
}
printf("\n");

// Em duas dimensões
char cmat[2][3] = {{'B','L','A'},{'B','O','S'}};
char* ppc;

ppc = (char*)cmat;

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%c", *(ppc + 3*i+j));
    }
    printf("\n");
}
printf("\n");

Na linha 18 o ponteiro pc passa a pontar para o array cvet, e consequentemente para o seu primeiro elemento, o caracter 'B'. Na linha 21 o conteúdo de pc, que é o endereço onde foi armazenado o caracter 'B', é incrementado de i e em seguida de-referenciado. Na primeira passada o valor de i é zero, portanto é dereferenciado para o valor 'B'. Nos passos seguintes, o endereço seguinte vai sendo de-referenciado para os outros caracteres armazenados no array original. É mais ou menos isso que internamente o compilador faz quando você utiliza a sintaxe cvet[i]. A abstração de array te dá uma forma mais amigável de tratar áreas contíguas de memória do que *(pc + i).

Mas se a abstração de array é mais simples, pra que utilizar aritmética de ponteiros?

Uma das respostas está a linha 26. Ela faz a mesma coisa que a linha 21, porém um pouco mais rápido. Na sintaxe da linha 21, ou de forma semelhante, na sintaxe de array, o acesso a um dado qualquer pode ser resumido de forma bem grosseira nos comandos:

  1. Tome o endereço base do array;
  2. Adicione ao endereço o valor do índice;
  3. De-referencie este novo endereço;

Já com aritmética de ponteiro ficaria assim:

  1. De-referencie este endereço;

O comando incremento (ou a conta feita com endereços) não vou contar porque é parte do loop, embora i++; seja mais rápido que a = b + c;. Agora imagine esse pequeno ganho de 66% aplicado em uma área de dados de 1 MB. Serão mais de 2 milhões de comandos a menos!

A técnica de utilizar um ponteiro para manipular uma área arbitrária de memória é utilizada geralmente em programação de baixo nível (mais próximo da máquina), manipulação de buffers e strings, entre outros truques sujos. Nas entranhas dos computadores, operações que varrem extensas áreas de memória, frequentemente são realizadas com aritimética de ponteiros. Nesse nível, Darwin reina supremo e só os mais preparados sobrevivem. A partir daqui a linguagem começa a dar um poder que só os puros de coração conseguem compreender.

Uma observação importante é que entre as linhas 31 e 38 a experiência é repetida com inteiros. Note que como os inteiros possuem 4 bytes, os incrementos são automaticamente feitos de 4 em 4 bytes, e não de 1 em 1, ou seja o incremento é automaticamente calculado para sizeof(tipo). Incrementar um ponteiro significa acessar a próxima área de memória semelhante ao dado atual, e não apenas o próximo endereço. Como o tamanho de um char é um byte, quando incremetamos um ponteiro para char, avançamos apenas 1 byte. Se incrementarmos um ponteiro para double, avançaremos 8 bytes, e assim por diante.

Outra observação é que uma matriz bidimensional pode ser "linearizada" conforme é mostrado nas linhas 40 a 52. Isso é útil, quando aplicável, para aproveitar melhor o cache do processador, por exemplo.

void*, o Pansexual dos ponteiros

Anteriormente eu disse que só era possível atribuir um array a um ponteiro que fosse para o mesmo tipo que os dados do array. Eu menti descaramente! O motivo, é que para alguém que desistiu do post antes deste tópico, é mais seguro acreditar que não pode :) !

Existem duas exceções à regra. A primeira é quando há uma conversão explícita de tipos e o ponteiro destino "acha" que está apontando para o tipo certo. Um exemplo está na linha 44 do código anterior.

A segunda é o caso dos ponteiros para void. Um ponteiro para void é um ponteiro que não faz exigências quanto ao tipo de dados que está na área de memória para o qual ele aponta. Ele é um ponteiro pra uma área genérica de memória, algo bem baixo nível.

Para utilizar um dado apontado por um ponteiro para void, antes de de-referenciá-lo, é preciso fazer um cast explícito para algum tipo válido, pois se um int* é de-referenciado para int e um char* é de-referenciado para char, adivinhe para que é de-referenciado um void*?

const int max = 6; 
char cvet[max] = {'B','L','A','B','O','S'};

void* pv = cvet;

// Erro de compilação.
//*pv
// Quanto vale sizeof(void)?

for (int i = 0; i < max; i++) {
    printf("%c", *(((char*)pv) + i));
}
printf("\n");

Ponteiro para void são utilizados quando precisa-se apontar para uma área genérica de memória sem ter controle/conhecimento do tipo de dados que essa área contém, ou em funções que não podem fazer suposições sobre os tipos de seus parâmetros, como é o caso da API da lib pthreads (link arbitrário).

Encerrando

Quanto mais nos aprofundamos nos tópicos sobre ponteiros, mais próximos da máquina ficamos. Boa parte do poder das linguagens C e C++ provém daí, e boa parte dos problemas também. A complexidade vai aumentando e os riscos também. Para muitos é aí que mora a diversão!

Links

TPM - Telepatia, Premonição e Milagre

Certa vez trabalhei com uma equipe que cunhou a seguinte pérola: "Pra trabalhar aqui é preciso ter TPM - Telepatia, Premonição e Milagre". Isso porque a equipe quase sempre trabalhava em duas cidades, tinha que prever as especificações de projeto que ainda não tivessem sido especificadas e por último fazer o Milagre acontecer. Os caras eram bons MESMO. Depois de alguns anos, hoje tive pistas de que posso ter encontrado outra equipe assim, ainda que em fase embrionária...

Guardadas as devidas proporções, vamos aos fatos. Faculdade técnica, aula de humanas. Não poderia haver combinação mais explosiva. 60% da nota do semestre viria de um seminário sobre parte do livro Gestão à Brasileira. Trabalho em grupo. Grupo de 8 pessoas. Además, já que a gestão é à brasileira, o prazo para confecção do trabalho também o é. Ok, tá certo que tivemos um adiamento de uma semana por motivos de força maior, mas uma semana a mais em um prazo irrisório, continua irrisório. Mas somos brasileiros e não desistimos nunca!

Eu sou um cara relativamente sortudo, de forma que pude escolher trabalhar com mais 7 pessoas de extrema competência. Tanto é que em todas as fases do projeto trabalho, todos estavam efetivamente envolvidos e produzindo, o que é mais incrível.

Nesta fase apareceram os primeiros telepatas. Seres superiores capazes de fazer o trabalho sem terem lido o texto base, apenas pegando as idéias telepaticamente dos que leram. Os telepatas restantes surgiram na hora de fazer os slides da apresentação, pois fizemos em pedaços meticulosamente encaixáveis, telepaticamente, óbvio!

Mas pessoas diferentes tem necessidades diferentes. Com 8 pessoas trabalhando em partes separadas, cada um utilizou sua suite Office® preferida (2000, 2003, 2007...). Só mesmo um trouxa inventou de usar aquela porcaria do Open Office, aquela merda.

Eis que para a surpresa de todos (inclusive do trouxa que vos fala), certa suite de escritório proprietária mostrou-se incompatível consigo mesma. Como assim Bial? Calma que o tio explica. Primeiramente a versão da SOP (suite office proprietária) onde faríamos a apresentação, especulava-se ser a 2000 (eu não fui conferir). Segundamente, eu não sei por que, eu não sei como e eu não me importo, quando tentávamos salvar na SOP 2007 um conjunto de slides no formato SOP 2003 dava uma merda federal. Era fonte preta que ficava branca, era tamanho de fonte que mudava e não me pergunte, eu não entendo do assunto, os slides com imagens e textos eram transformados em imagens.

Até aí tudo bem (pelo menos pra mim que não tive que refazer slide). Montaram uma operação de guerra com pacotes instaláveis via pendrive, cds e tudo mais.

Quarta feira tensa. Organicamente muito ruim pra mim. Meu dia foi começar a ficar suportável depois das 13:00hs. O que sobrou de mim, repassava o texto, fazia resumos e torcia para chegar inteiro até o momento da apresentação. Meu dia de vidente. Levo note, não levo note? Vou levar o Eee. Maldito peso extra! Outro vidente levou seu note com software proprietário. Os outros confiaram na operação de guerra com tripla redundância: CD, pendrive com SOP instalável e MP4.

Eis que aos 45 do segundo tempo descobrimos que a besta que vos fala replicou 3 textos em um slide. Vamos alterar, moleza! Infelizmente a vesão da SOP do outro vidente era mais recente que versão dos arquivos. Putz, o que é mesmo interoperabilidade? E INTRAoperabilidade? E Retrocompatibilidade? Que pena, pelo visto as SOPs em questão desconhecem todos esses conceitos. Resultado: Fodeu. Não me pergunte como, texto foi salvo como imagem, fonte trocou de cor e o escambau.

Já quase conformados com a idéia de ter que reformatar todo o trabalho em 15 min, resolvi abrir o arquivo em formato proprietário com aquela bosta de Open Office. Resultado: Milagre! O Open Office conseguiu recuperar boa parte do trabalho. Daí, 3 ou 4 ajustes e os slides estavam corrigidos. Só tinha um pequeno problema. A máquina onde faríamos a apresentação não entendia ODF, e salvar no formato da SOP destruía a formatação. Ah Eee, você tem saída VGA! Despluga, pluga, nada. Reboot plugado e tã-dãm! Simplesmente funciona. Eee plugado no projetor e funcionando.

Moral da história: Open Office + Eee = Trabalho apresentado, nota garantida. Até o próximo semestre (nesta matéria).

Não me pergunte porque foi tão difícil dominar uma suite office. Não me interessa. Eu não quero ler um manual de 6000 páginas pra poder fazer um conjunto de slides simples, somente com texto e imagens, sem efeitos. Não é isso que os defensores do software proprietário bradam? Que simplesmente funciona? Que o Open Office é uma merda? Ok, pode até ser, mas pelo menos é uma merda que fede, que faz que se propõe a fazer. Não me interessa qual versão roda na faculdade ou na sua casa. Eu quero poder compartilhar arquivos sem surpresas com meus 7 colegas de grupo, cada um com o software que melhor lhe servir. Eu quero interoperabilidade. Não me interessa se o Open Office não é a borboleta coloria faiscante sueca que você quer. Eu quero que simplesmente funcione. Que seja simples. Que seja intuitivo. Que eu não precise perder tempo refatorando slide. Tempo esse que eu poderia estar estudando Cálculo, Grafos ou o que mais fosse.

Não me interessa se você me chamar de burro por não conseguir fazer slide em suite sofisticada. Eu não quero ter que reservar uma área do meu cérebro para descobrir como usar um software de fazer slide, pra aula de humanas (com o devido respeito). Eu quero gastar meu cérebro entendendo como se comporta uma derivada parcial com n variáveis num espaço de m dimensões, pra entender e argumentar quando um livro de cálculo trás curvas de níveis desenhadas ao contrário. Eu quero gastar meu cérebro entendendo que apesar de uma referência existir e ocupar memória, um operador nunca opera sobre ela.

Não faço Ciência da Computação pra aprender a ser operador de planilha ou arrastador de caixinha colorida.

Creio que devemos umas cervejas ao pessoal do Open Office pelo excelente trabalho de engenharia reversa sobre formatos proprietários. Você poderiam dar umas aulas para o pessoal de certa empresa de software, pra que eles aprendam a ser mais compatíveis com eles mesmos. A partir de hoje vocês ganharam por tempo indeterminado um banner (feio que dói) no blog, na faixa.

Ao pessoal do grupo meus sinceros agradecimentos por um trabalho bem feito e bem apresentado. É sempre uma honra poder trabalhar com os melhores.

Ponteiros e referências em C++ parte 2

Continuando com essa tentativa de série de posts sobre ponteiros e referências, que começou falando sobre Ponteiros, hoje abordaremos Referências.

Uma referência é um nome alternativo para um objeto, um alias. Aliás, quem diz isso não sou eu, mas o próprio Bjarne Stroustrup, lá pelo tópico 5.5 do seu livro, cujo conteúdo é tão bom que só fui reparar hoje que na capa tem uma onda quebrando em forma de C. Gostei dessa capa!

Declarando Referências

Uma referência para um determinado tipo é declarada acrescentando-se o caracter & (ê comercial ou ampersand) após o nome do tipo da referência. Note que da mesma forma que na declaração de ponteiros, estamos tratando o & como um caracter de notação e não um operador. Não há um operador de declaração de ponteiros ou referências. É apenas uma notação da linguagem. Dado um tipo T, a expressão T& significa Referência para T. Por exemplo:

// Declara uma variável inteira original.
// Também poderia ser char, float, etc.
int i;

// Declaram referências para a variável i.
// Todas as formas são equivalentes, mas preferimos a primeira.
// Note que os tipos das referências tem que ser os mesmos
// que os das variáveis referenciadas.
int& ra     = i;
int &rb     = i;
int & rc    = i;

// Diferentemente de ponteiros, a linha abaixo gera um
// erro de compilação. Você saberia dizer por quê?
int &r1, &r2;

Embora em C++ as referências lembrem um pouco os ponteiros, eles são bem diferentes. Ao contrário do que acontece com ponteiros, uma referência precisa ser inicializada durante sua declaração. Tentar declarar uma referência sem inicializá-la gera um erro em tempo de compilação, ou seja, o compilador vai cuspir na sua cara "Perdeu playboy, perdeu!". A única exceção a essa regra é para referências declaradas como extern, pois elas serão inicializadas em algum outro ponto do programa, mas isso é outro papo.

Se você tentar trapacear declarando uma referência extern sem inicializá-la o compilador não vai reclamar. Se ela nunca for utilizada, ok. Se o compilador for esperto e notar que ela não será utilizada, pode até removê-la da lista de símbolos. Mas se ela for utilizada em qualquer parte do programa, o linker vai rir da sua cara gargalhando "undefined reference to 'nome_da_variavel'". Portanto faça as coisas direitinho. Declarou uma referência? Então iniclalize-a. extern? Tem certeza que sabe o que está fazendo?

A lógica por trás disso é que uma referência foi planejada para ser um nome para alguma coisa. Se você não a inicializar, ela não será nome para nada, então não faz sentido. Um detalhe muito importante é que inicializar uma referência não é atribuir um valor a ela. Como o prório Bjarne diz, nenhum operador opera sobre referências, ou seja, não há como atribuir, somar, subtrair, etc com uma referência. Como uma referência é um apelido para um objeto, cada operador atua nese objeto, e não na referência. Uma vez inicializada, uma referência sempre referenciará o mesmo objeto.

A primeira armadilha com referências, assim como ponteiros está na questão do tamanho. Enquanto o tamanho de um ponteiro (e de outras estruturas de dados) pode ser obtido com o operador sizeof, o tamanho de uma referência não pode ser obtido por técnicas convencionais, já que ao aplicar o operador sizeof em uma referência, estamos na verdade, aplicando o operador no objeto ao qual ela se referencia, conforme vimos no parágrafo anterior.

char    c;
int     i;
double  d;

char&   rc = c;
int&    ri = i;
double& rd = d;

// O operador sizeof diz o tamanho do tipo do seu argumento.
cout << "Size of char:    " << sizeof(c) << " bytes" << endl;
cout << "Size of int:     " << sizeof(i) << " bytes" << endl;
cout << "Size of double:  " << sizeof(d) << " bytes" << endl;

cout << endl;

cout << "Size of char&:   " << sizeof(rc) << " bytes" << endl;
cout << "Size of int&:    " << sizeof(ri) << " bytes" << endl;
cout << "Size of double&: " << sizeof(rd) << " bytes" << endl;

As técnicas para obter os tamanhos das referências fogem ao escopo deste texto, mas pretendo falar sobre elas futuramente. Por hora, vou apenas dizer sem demonstrar que, quando aplicável, o tamanho de uma referência é idêntico ao tamanho de um ponteiro.

A parte boa da história é que frequentemente os tamanhos de ponteiros e referências são bem menos relevantes que os tamanhos dos objetos apontados ou referenciados. Eu particularmente, nunca precisei utilizar essa informação, mas para o desenvolvimento em arquiteturas diferentes ou para sistemas embarcados, isso já se torna mais interessante.

Utilizando Referências

O uso de referências já é bem mais simples que o de ponteiros, pois não são necessários operadores de extração de endereços ou de de-referenciação. Variáveis referências são utilizadas como variáveis comuns, lembrando que uma operação feita numa referência sempre afeta o objeto referenciado.

int x, y;
int& rx = x;
int& ry = y;

x = 13;
y = 10;

rx = 42;    // A atribuição é automaticamente aplicada em x.
ry++;       // O incremento é automaticamente aplicado em y.

cout << "x:  " << x << endl;    // Valor de x.
cout << "y:  " << y << endl;    // Valor de y.
cout << "rx: " << rx << endl;   // Valor de rx == x.
cout << "ry: " << ry << endl;   // Valor de ry == y.

Utilizar uma referência é tão transparente para o programador, que ele nem precisa saber que é uma referência. Simplesmente usa como se fosse uma variável ordinária. Com minha imaginação limitada não vejo como alguém consiga ser excomungado por utilizar referências inadequadamente. Embora sempre tenha algum espírito de porco que consiga fazer merda com referências, é muito mais fácil levar rasteira de Saci ao manusear ponteiros.

Endereços e Referências

Do ponto de vista de Murphy, as referências são feias, chatas e bobas, pois existe pouca coisa potencialmente perigosa a se fazer com elas. Nem mesmo quanto a endereçamento há muitas surpresas (será?). Considere o código abaixo:

int a = 10;
int b = 13;

int& ra = a;
int& rb = b;

ra = 42;
rb = 7;

cout << "ra  = " << ra << endl;// O valor de ra == 42.
cout << "rb  = " << rb << endl;// O valor de rb == 7.
cout << "a   = " << a << endl; // O valor de a == ra == 42.
cout << "b   = " << b << endl; // O valor de b == rb == 7.

cout << "&a  = " << &a << endl; // O endereço de a.
cout << "&b  = " << &b << endl; // O endereço de b.
cout << "&ra = " << &ra << endl;// O endereço de ra == &a. 
cout << "&rb = " << &rb << endl;// O endereço de rb == &b.

Nas linhas 17 e 18 eu declaro duas variáveis inteiras (poderia ter feito esse exemplo com uma só...). Nas linhas 20 e 21 eu declaro duas referências, referenciando as duas variáveis anteriores. A partir desse momento as variáveis ra e rb são apenas apelidos para as variáveis a e b. Como vimos anteriormente, qualquer operador aplicado às referências, na verdade atua nos objetos referenciados, portanto cada par variável-referência possui sempre o mesmo valor.

A parte interessante a se notar é que os endereços das referências são exatamente os mesmo endereços das variáveis que elas referenciam, ao contrário dos ponteiros que tinham seus próprios endereços. Por quê? Porque você caiu na Pegadinha do Malandro. Lembra daquele papo de nenhum operador ser aplicável a uma referência? Pois bem, o & utilizado aqui é o operador "endereço de", que também é aplicado diretamente nas variáveis originais e não nas referências. As mesmas técnicas ninja utilizadas para obter os tamanhos das referências são necessárias para se obter os endereços delas.

Novamente a parte divertida disso é que o mecanismo de referências foi planejado para ser o mais transparente possível para o programador. Tamanhos e endereços físicos de referências são informações irrelevantes do ponto de vista da programação "normal" em C++.

Diferenças e semelhanças entre Ponteiros e Referências

Tanto ponteiros como referências são mecanismos de indireção em C++, ou seja, são mecanismos utilizados para que a partir de um dado símbolo (variável) eu seja capaz de manipular outro objeto.

Os principais usos para ponteiros são geralmente relacionados com gerenciamento e manipulação dinâmica de memória, criação e destruição de objetos de formas e em momentos especiais, bem como "passagem de parâmetros por referência" (assim entre aspas mesmo). Já as referências são mais utilizadas em passagem por referência (por que será?) e sobrecarga de operadores.

Basicamente, tudo que podemos fazer com referências pode ser emulado com ponteiros. Já a recíprocra não é sempre verdadeira. A grande vantagem delas no entanto, é o fato de tornarem a indireção completamente transparente ao programador.

Uma alegoria interesante para ajudar a entender melhor as diferenças entre ponteiros e referências é o apelido. Por exemplo: Imagine que um pessoa, digamos, Carlos Caetano Bledorn Verri, seja a nossa variável original, o objeto. Dunga já é uma referência a Carlos Caetano Bledorn Verri, pois é um apelido para o mesmo objeto, são a mesma pessoa. Já a Mãe do Dunga (ou do Carlos Caetano Bledorn Verri, tanto faz), que é contantemente citada pela torcida, pode ser considerada um ponteiro para ele, pois dados determinados operadores de vocabulário, um elogio feito a ela, na verdade é indiretamente direcionado a ele.

Links

Indicação dos leitores

Esse merece um post:
Khaoz on April 29th, 2009 09:48 Não resisti e vou “homenagiá o curintia”: http://www.youtube.com/watch?v=Q9UB8U7U_S0
Sensacional!!!

Ponteiros e referências em C++ parte 1

Ponteiros e referências são dois conceitos importantíssimos em ciência da computação. Eles aparecem em muitas linguagens de programação com uma roupagem ou pouco diferente, mas basicamente o tratamento é o mesmo. Em C++, habilidades com ponteiros são fundamentais.

Ponteiro é um tipo especial de dado, cujas variáveis declaradas desse tipo podem apontar (ou não) para algum outro dado na memória. Uma variável do tipo ponteiro, guarda o endereço de um outra coisa na memória. Essa coisa pode ser alguma variável, constante, início de um conjunto de dados, uma função, entre outras.

Declarando Ponteiros

A declaração de um ponteiro é normalmente feita acrescentando-se um * antes do nome da variável. Uma notação muito utilizada cola o * ao nome do tipo para dizer simbolicamente que se está declarando um "ponteiro para o tipo", ao invés de uma "variável ponteiro", mas elas são equivalentes. Por exemplo:

// Ponteiro para um int.
int* ptr_num1;
 
// Ou assim.
int *ptr_num2;
 
// Ponteiro para um double.
double* ptr_double1;
 
// Ou assim.
double *ptr_double2;

Depois de declarado, o ponteiro existe, possui tamanho e consequentemente ocupa lugar na memória. O tamanho de um ponteiro geralmente é igual ao número de bits da máquina/sistema em questão, por exemplo 4 bytes num sistema 32 bits.

char    c, *pc;
int     i, *pi;
double  d, *pd;

// O operador sizeof diz o tamanho do tipo do seu argumento.
cout << "Size of char:    " << sizeof(c) << " bytes" << endl;
cout << "Size of int:     " << sizeof(i) << " bytes" << endl;
cout << "Size of double:  " << sizeof(d) << " bytes" << endl;

cout << endl;

cout << "Size of char*:   " << sizeof(pc) << " bytes" << endl;
cout << "Size of int*:    " << sizeof(pi) << " bytes" << endl;
cout << "Size of double*: " << sizeof(pd) << " bytes" << endl;

Utilizando Ponteiros

Após a sua declaração, o conteúdo de um ponteiro é um valor aleatório, lixo de memória, como qualquer variável. Para que ele seja útil precisamos atribuir algo a ele. Como um ponteiro guarda um endereço de memória, precisamos atribuir a ele um endereço válido.

int x, y;
int *px, *py;

x = 13;
y = 10;

px = &x;
py = &y;

*px = 42;
(*py)++;

cout << *px << endl;
cout << *py << endl;
cout << (*px) * (*py) << endl;

O operador & (ê comercial ou ampersand) antes do nome de uma variável significa "o endereço de", até porque o seu nome é "endereço de".

O que está à esquerda da igualdade tem que poder guardar exatamente o mesmo tipo do que estiver do outro lado da igualdade. Um ponteiro guarda um endereço. Portanto para atribuir um endereço a um ponteiro, neste caso, é preciso obter um endereço com o operador &.

Exceto nos caso em que há conversão implícita de tipos (como entre tipos numéricos), atribuir a uma variável algo com um tipo diferente do seu, causa um erro de compilação. Felizmente!!!

Depois que um ponteiro já apontar para um endereço válido, poderemos acessar o conteúdo desse endereço com o operador * (Asterisco), cujo nome formal é "operador de de-referência".

Note que o asterisco em uma declaração significa "ponteiro para o tipo" e no contexto da leitura ou escrita do conteúdo de um ponteiro, significa "o conteúdo do endereço" ou "o conteúdo apontado por". Também cuidado para não confundir com o símbolo para produto aritmético. Para não confundir, lembre-se daquela tabela enorme de precedência dos operadores, que você só passou o olho e deixou cair no esquecimento. Nela você vai encontrar que o operador de de-referência tem precedência sobre o operador de multiplicação. Feito isso você vai se lembrar que o operador de de-referência é avaliado ANTES do operador de multiplicação.

int a, b, c, *pa, *pb;
a = 2;
b = 3;

pa = &a;
pb = &b;

c = *pa * *pb;
c = *pa * * pb;
c = * pa * * pb;
c = *pa**pb;
c = (*pa) * (*pb);

cout << "Valor da Pegadinha: " << c << endl;

Assim quando aquela prova de algoritmos te perguntar o que faz qualquer uma das últimas 5 linhas do exemplo, você vai dizer que ela multiplica os conteúdos apontados por pa e pb. Não vai mais precisar pegar DP por causa disso! E de quebra na vida real, se você realmente precisar fazer isso, você vai salvar suas 4 gerações passadas e as 5 futuras da excomunhão, se escolher usar a última linha.

Tudo que é possível fazer com uma variável comum é possível fazer com o conteúdo apontado por um ponteiro, seja leitura ou escrita (a menos é claro que o ponteiro seja const, mas isso é outro papo...).

Endereços e Ponteiros

Tomemos como exemplo o código abaixo:

int a, b;
int *pa, *pb;

printf("Antes de qualquer atribuição:\n");

printf("&a  = %p\n", &a); // Endereço válido dado pelo SO.
printf("&b  = %p\n", &b); // Endereço válido dado pelo SO.
printf("&pa = %p\n", &pa);// Endereço válido dado pelo SO.
printf("&pb = %p\n", &pb);// Endereço válido dado pelo SO.

printf("\n");

printf("a   = %d\n", a);  // Conteúdo não inicializado (aleatório).
printf("b   = %d\n", b);  // Conteúdo não inicializado (aleatório).
printf("pa  = %p\n", pa); // Conteúdo não inicializado (aleatório).
printf("pb  = %p\n", pb); // Conteúdo não inicializado (aleatório).

// Tente descomentar as linhas abaixo, recompilar e rodar.
//printf("*pa  = %d\n", *pa); // O que será (*aleatório)?
//printf("*pb  = %d\n", *pb); // O que será (*aleatório)?

printf("\nApós as inicializações dos ponteiros:\n");

pa = &a;
pb = &b;

printf("&a  = %p\n", &a); // Mesmo endereço válido dado pelo SO.
printf("&b  = %p\n", &b); // Mesmo endereço válido dado pelo SO.
printf("&pa = %p\n", &pa);// Mesmo endereço válido dado pelo SO.
printf("&pb = %p\n", &pb);// Mesmo endereço válido dado pelo SO.

printf("\n");

printf("a   = %d\n", a);  // Conteúdo não inicializado (aleatório).
printf("b   = %d\n", b);  // Conteúdo não inicializado (aleatório).
printf("pa  = %p\n", pa); // Conteúdo inicializado (&a).
printf("pb  = %p\n", pb); // Conteúdo inicializado (&b).

printf("*pa = %d\n", *pa);// Conteúdo apontado por pa (*pa == a).
printf("*pb = %d\n", *pb);// Conteúdo apontado por pb (*pb == b).

printf("\nApós as inicializações dos inteiros:\n");

a = 10;
b = 13;

printf("&a  = %p\n", &a); // Mesmo endereço válido dado pelo SO.
printf("&b  = %p\n", &b); // Mesmo endereço válido dado pelo SO.
printf("&pa = %p\n", &pa);// Mesmo endereço válido dado pelo SO.
printf("&pb = %p\n", &pb);// Mesmo endereço válido dado pelo SO.

printf("\n");

printf("a   = %d\n", a);  // Conteúdo inicializado (10).
printf("b   = %d\n", b);  // Conteúdo inicializado (13).
printf("pa  = %p\n", pa); // Conteúdo inicializado (&a).
printf("pb  = %p\n", pb); // Conteúdo inicializado (&b).

printf("*pa = %d\n", *pa);// Conteúdo apontado por pa (*pa == a == 10).
printf("*pb = %d\n", *pb);// Conteúdo apontado por pb (*pb == b == 13).

printf("\nAlterando os valores através dos ponteiros:\n");

*pa = 7;
*pb = 42;

printf("&a  = %p\n", &a); // Mesmo endereço válido dado pelo SO.
printf("&b  = %p\n", &b); // Mesmo endereço válido dado pelo SO.
printf("&pa = %p\n", &pa);// Mesmo endereço válido dado pelo SO.
printf("&pb = %p\n", &pb);// Mesmo endereço válido dado pelo SO.

printf("\n");

printf("a   = %d\n", a);  // Conteúdo inicializado (a == *pa ==  7).
printf("b   = %d\n", b);  // Conteúdo inicializado (b == *pb == 42).
printf("pa  = %p\n", pa); // Conteúdo inicializado (&a).
printf("pb  = %p\n", pb); // Conteúdo inicializado (&b).

printf("*pa = %d\n", *pa);// Conteúdo apontado por pa ( 7).
printf("*pb = %d\n", *pb);// Conteúdo apontado por pb (42).

As linhas 18 e 19 declaram respectivamnete dois inteiros e dois ponteiros para inteiros.

As linhas 23 a 26 mostram os endereços das minhas variáveis. Esses endereços foram dados pelo sistema operacional e são fixos enquanto essas variáveis existirem. Uma variável não muda de endereço durante a execução do programa, mas pode mudar entre uma execução e outra. Quem define qual é esse endereço é o sistema operacional no momento da execução do programa. O compilador só sabe que serão necessários x bytes para cada variável, e qual a interpretação (tipo) que o programa vai dar a esse espaço.

As linhas 30 a 33 mostram os conteúdos dessas variáveis, que ainda não foram inicializadas. É lixo, o último valor que alguém (não sei quem) colocou naqueles lugares da memória do computador e que agora pertencem às minhas variáveis.

Note que não é seguro descomentar as linhas 36 e 37 pois elas tentam acessar os conteúdos apontados por pa e pb, que ainda não foram inicializados. Durante essa tentativa, o programa vai tentar interpretar aqueles valores de lixo aleatório como se fossem endereços de variáveis int válidos. Dependendo do compilador, do SO utilizado e da sua sorte, pode ser algo inofensivo ou não. O certo é que o resultado dessas linhas é imprevisível e fonte certa de problemas.

Nas linhas 41 e 42 os ponteiros são inicializados com os endereços das variáveis inteiras a e b. A partir daí os prints são repetidos.

Veja que os endereços dos ponteiros não mudam, o que já era esperado. O que mudam são os seus conteúdos. As variáveis pa e pb recebem os endereços de a e b respectivamente. Note que o conteúdo dos ponteiros é exatamente o valor do endereço das variáveis a e b que foram dados pelo SO no início do programa. Confira com os prints anteriores. O conteúdo apontado por pa e pb é o mesmo conteúdo de a e b, que ainda não foram inicializados, ou seja, lixo. Porém, diferentemente da etapa anterior, agora os ponteiros apontam para endereços válidos (os endereços de a e b) e ainda que os conteúdos de a e b sejam lixo de memória, os ponteiros agora apontam para variáveis que já foram alocadas.

Nas linhas 61 e 62 as variáveis inteiras são inicializadas com os valores 10 e 13, e os prints repetidos denovo.

Desta vez os coteúdos de a e b não são mais lixo. São valores devidamente conhecidos. Note que não há nenhuma alteração no endereçamento das variáveis em relação às etapas anteriores, apenas nos valores.

Veja também que depois que a e b foram inicializados, o conteúdo apontado por pa e pb também foram alterados automaticamente. Isto acontece justamente porque pa e pb apontam para as mesmas áreas de memória utilizadas pelas variáveis pa e pb. Isso significa que o conteúdo apontado por um ponteiro pode ser modificado alterando-se o conteúdo das variáveis originais apontadas por ele. E finalmente as linhas 81 a 97 mostram que o inverso também vale, ou seja, alterando-se o conteúdo apontado por um ponteiro, altera-se automaticamente o conteúdo das variáveis originais apontadas por eles.

É mais fácil entender isso pensando nas frases em português do que olhando para o código.

Por enquanto...

Este foi o básico da operação com ponteiros. Ainda existem coisas interessantes para serem vistas, como ponteiros para funções, por exemplo, mas isso já é considerado um tópico mais avançado. Na sequência desta série, que está planejada para 4 posts, veremos referências, diferenças e semelhanças entre ponteiros e referências e por último ponteiros para funções.

Links