Ir para conteúdo

Algumas regras para quem está aprendendo C


fredericopissarra

Posts Recomendados

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...

Link para o comentário
Compartilhar em outros 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...

Link para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...