Jump to content
Sign in to follow this  
fredericopissarra

Algumas regras para quem está aprendendo C

Recommended Posts

Posted (edited)

Isto é uma declaração:

char *p;
// O asterisco diz que a variável p vai ser usada
// para conter o endereço de um array de chars.
// Ou seja, p é um ponteiro!

Já isso é uma expressão:

c = *p;
// O asteristo é um OPERADOR que usa o endereço
// contido em p para ler um caracter contido neste endereço.
// É um acesso INDIRETO.

Existe, claramente, diferenças entre declarar e usar símbolos em C (e em C++). E todo uso de símbolos é feito por expressões.

É possível misturar os dois:

int x = 10;
// equivalente a:
//  int x;  /* declaração */
//  x = 10;  /* expressão */

Mas, mesmo assim, são duas coisas separadas.

Vejamos outra mistura usando o símbolo p, declarado acima:

int *pi = (int *)p;
// O mesmo que:
//  int *pi;  /* declaração */
//  pi = (int *)p;  /* expressão */

Na expressão, temos 2 operadores: O de conversão de tipo (casting) e o de atribuição. Aqui o operador (int *) converte o tipo do ponteiro p para int * (ponteiro para um array de ints) e atribui o conteúdo de p (não de *p -- não tem operação de indireção aqui!) à variável pi, que também é um ponteiro, para um array de tipo diferente...

Já a expressão:

int x = *(int *)p;

Tem 3 operadores (ou operações!): O operador de conversão (int *), como antes, o operador * (de indireção) e o operador = de atribuição.

Esse asterisco ai, antes do casting, não é o mesmo que é usado na declaração de p... É uma operação (assim como soma, subtração, multiplicação, divisão, ...). O operador de indireção usa o conteúdo da variável p como endereço de memória.

OPA! E se tivermos uma operação de indireção (*) e uma de multiplicação (também *) na mesma expressão?

Assim como quanto você tem duas operações de multiplicação e uma de soma, como em x = a * b + c * d, o compilador determinará quais operações devem ser feitas primeiro pela precedência dos operadores. Um operador * depende do uso de uma variável ponteiro e tem precedência maior que o operador * multiplicativo... Assim, numa expressão do tipo:

int x;
x = 10 * *pi;

A primeira sub-expressão a ser resolvida é a que usa * como indireção, depois o segundo * será usado como multiplicação (neste caso):

// usando parenteses para mostrar a precedência...
x = (10 * (*pi));

PS: A associatividade também deve ser levada em conta... Assim como na expressão x=a*b+c*d a precedência é das multiplicações, a associação é da direita para esquerda, ou seja, (a*b) é feita primeiro, depois (c*d)... No caso de inteiros isso não é importante porque a propriedade comutativa é obedecida sempre, mas em ponto flutuante isso importa (procure pela discussão sobre ponto flutuante aqui no forum!)...

O operador [] é um atalho para operações com o operador de indireção

Quando você declara e define:

char s[5] = "fred";

Como vimos, isso é um atalho... O que fizemos aqui foi declarar um símbolo s que aponta para um array de 5 chars... Yep... s é um ponteiro! Isso é quase similar a fazer:

char *s = "fred";

Isso só não é igual porque, no primeiro caso, você está pedindo para o compilador alocar espaço para o array e copiar o array literal contendo 5 chars para dentro dele (incluindo o '\0' final)... No segundo caso você diz para o compilador que s aponta para um array de 5 chars, mas ele é constante, imutável, read-only. Se tentar usar uma expressão como *s='\0', vai tomar um "segmentation fault" na cara!

O detalhe é que se ambas as declarações declaram ponteiros, a expressão s[n] é a mesma coisa que *(s+n). Ou seja, o operador [] é um atalho para uma expressão usando o operador de indireção! Isso causa o interessante efeito colateral de que não precisarmos usar o operador [] numa ordem particular de ponteiro[índice]. Ao invés de escrevermos s[n] poderíamos escrever n, porque *(s+n) é a mesma coisa que *(n+s).

Se um ponteiro é um valor inteiro "comum", porque o tipo na declaração?

Lembre-se que te disse que um ponteiro aponta para um array. Um array tem um tipo associado porque o compilador precisa saber onde começa e termina cada um dos seus elementos... Num array de char, cada item tem 1 byte de tamanho. Num array de int, cada item tem 4 bytes...

No caso de um ponteiro, o tipo serve para que o compilador saiba como vai fazer as operações aritméticas com o valor contido no ponteiro... Por exemplo:

int a[3] = { 1, 2, 3};
int *p = a;  // p = endereço do array a (ou a[0]).
p++; // qual é o endereço contido em p agora?

Vamos considerar do início do array como sendo 0x400000. Este é, obviamente, o endereço do elemento de índice 0, ou a[0] e o colocamos na variável p., simplesmente aproveitando-nos do fato de que o nome do array é o ponteiro para o primeiro elemento. Poderíamos fazer p = &a[0], que é a mesma coisa... 

Mas, depois de incrementar p, qual será o endereço contido nele? Não pode ser 0x400001 porque um int tem 4 bytes de tamanho. Ou seja, ao incrementar p, que foi declarado como um ponteiro para um array de int, o compilador sabe que deve, nas adições e subtrações envolvendo um ponteiro desse tipo, multiplicar o offset por 4 (ou sizeof(int)).

Este é o motivo do tipo estar associado ao ponteiro.

O que significa dois ou mais asteríscos na "declaração"?

Ou, o que é:

char **pp;

Se um asterisco diz ao compilador que a variável contém o endereço de um array do tipo especificado, dois asteriscos diz que a variável contém o endereço de um array de endereços para arrays do tipo especificado (ou, ele contém o endereço de um array de endereços para arrays do tipo). Esse ponteiro é usado em uma dupla indireção... Três asteriscos (bem raro) é o endereço de um array de endereços para arrays de endereços para o tipo...

Em teoria você pode ter tantas indireções quanto quiser, mas, é claro, isso vai ficando cada vez mais complicado de entender... Na prática, nunca vi mais que 3 níveis!

Graças a esses níveis de indireção é que tanto faz declarar main() em qualquer uma dessas formas:

int main(int argc, char *argv[]);
int main(int argc, char **argv);

Ambos os argv são ponteiros para arrays de ponteiros para chars. Neste caso, os argumentos são diretamente equivalentes porque não são declarações usadas para alocação de espaço, mas para estipular o tipo de argumento... Claro, esses argumentos serão convertidos em variáveis locais da função, mas mesmo assim, a alocação será feita na pilha, que nunca é read-only...

Edited by fredericopissarra
  • Curtir 2

Share this post


Link to post
Share on other sites

E, sim... uma chamada de função é uma expressão! Todo operador () que seja precedido por um símbolo ou ponteiro é uma chamada de função:

Considere as declarações e usos:

// DECLARAÇÃO de DEFINIÇÃO de uma função.
int f(int x) { return 2*x; }

int (*fptr)(int);  // DECLARAÇÂO de um ponteiro para uma função.

fptr = f; // EXPRESSÃO que inicializa o ponteiro fptr para o endereço da função f.

x = f(2); // EXPRESSÃO: () é precedido do símbolo f, daí é uma subexpressão de chamada de função.
y = fptr(2); // EXPRESSÃO: () é precedido do ponteiro para uma função, daí é uma subexpressão de chamda de função.
fptr2 = (fptr + 1); // EXPRESSÃO: () NÃO é precedido de símbolo ou ponteiro, daí é um operador de resolução de precedência... :)

Lembre-se... C trabalha sempre com DECLARAÇÃOES e USOS... as duas coisas são diferentes...

  • Curtir 1

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...