Em C, * é um operador ou parte de um tipo em uma declaração?

9

Em C, *é chamado operador indireto ou operador de desreferência. Entendo como funciona quando é usado em uma declaração. Faz sentido escrever *pou * p, considerando que é um operador unário.

No entanto, às vezes em uma declaração, a *é usado.

void move(int *units) { ... }

ou

int *p = &x;

Parece-me estranho que um operador seja usado aqui. Você não pode fazer coisas como

void move(int units++) { ... }

onde ++também é um operador unário. Esse *também é o operador de indireção ou *tem um significado diferente, onde diz algo sobre o tipo do ponteiro? (Se for esse o caso, estou pensando em usar, por exemplo int* units, declarações e de int x = *p;outra forma para maior clareza.)

Em esta resposta é dito que

O padrão C define apenas dois significados para o operador *:

  • operador indireto
  • operador de multiplicação

Eu também vi pessoas alegando que isso *é realmente parte do tipo. Isso me confunde.

Torm
fonte
1
Em void move(int *units) { ... }, é um operador indireto. Considerado parte do tipo, também pode ser escrito void move(int* units) { ... }, embora eu prefira o estilo anterior. Você lê os dois como "int ponteiro". Veja também stackoverflow.com/a/8911253
Robert Harvey

Respostas:

16

Os menores pedaços da linguagem C são símbolos lexicais , tais como palavras-chave (por exemplo int, if, break), identificadores (por exemplo move, units) e outros.

*é um elemento lexical chamado pontuador (como por exemplo {}()+). Um pontuador pode ter significados diferentes, dependendo de como é usado:

Um pontuador é um símbolo que possui significado sintático e semântico independente. Dependendo do contexto, pode especificar uma operação a ser executada (...), caso em que é conhecida como operador

No capítulo C11, padrão 6.5, sobre expressões, *é definido como operador unário (seção 6.5.3.2, para indireção) e como operador multiplicativo (seção 6.5.5), exatamente como você descreveu. Mas *só é interpretado como operador de acordo com as regras gramaticais que fazem expressões válidas.

Mas há também um capítulo 6.2 sobre conceitos, que explica que ponteiros para um tipo são tipos derivados. A seção 6.7.6.1 sobre declaradores de ponteiros é mais precisa:

se, na declaração '' T D1 '', D1 tiver o formato
* type-qualifier-list-opt D
e o tipo especificado para ident na declaração '' T D '' for '' derivado-declarator-type-list T '', o tipo especificado para ident é '' ponteiro-de-lista-de-tipo-declarador-derivado-qualificador-lista-ponteiro para T ''.

Isso realmente faz *parte de um tipo derivado ( *significa "ponteiro", mas sempre é necessário outro tipo para dizer que tipo de coisa aponta para alguma coisa).

Observação: Não há contradição nisso. Você usa esse elemento diferenciado da linguagem lexical todos os dias quando fala inglês: "um identificador " designa algo e " manipular " significa fazer algo, dois usos gramaticais completamente diferentes do mesmo elemento lexical.

Christophe
fonte
Isso significa que essa interpretação é apenas uma maneira comum de examiná-la (a partir da outra resposta): int * uma declaração pode ser interpretada como: declare uma variável, chamada a, que quando desreferenciada é do tipo int. -Quando na verdade o * tem um significado independente em uma declaração
Torm
@ Torta exatamente!
Christophe
7

Em C, o indireto em uma declaração é melhor lido como parte da variável do que parte do tipo. Se eu tiver:

int *a;

essa instrução pode ser interpretada como: declare uma variável denominada aque quando desreferenciada é do tipo int.

Isso é importante quando várias variáveis ​​são declaradas ao mesmo tempo:

int *a, b;   (1)
int* a, b;   (2)

Ambas as declarações são equivalentes, e em ambos ae bnão são os mesmos tipos - aé um ponteiro para um int e bé um int. Mas declaraction (2) parece que o "tipo de ponteiro" faz parte do tipo quando na verdade é a declaração de *a.

Erik
fonte
Esse é um ponto muito bom para trazer isso à tona, porque geralmente há uma confusão entre 1 e 2. No entanto, o tipo de a é "ponteiro para int", portanto, mesmo se o * apenas se aplica a a, é definitivamente parte do tipo . Você poderia, por exemplo, usá-lo em um typedef (o que não poderia ser feito para um inicializador).
Christophe
Procurado por um bom argumento para decidir entre as duas declarações, isso o acertou em cheio. Raramente declaro variável como uma lista, mas ainda assim, a partir de agora adotarei a primeira forma como a mais clara em todos os casos. Obrigado !
Newtopian 23/08/16
4

Para adicionar às outras respostas corretas:

É apenas uma expressão de declaração que reflete possíveis expressões de uso. IIRC, acho que Kernigan ou Ritchie chamaram isso de experimento! Aqui está um texto de apoio marginal:

Nos anos 70, quando Kernighan e Ritchie estavam escrevendo The C Programming Language, eles eram bastante honestos sobre como as declarações podem rapidamente se tornar ilegíveis a olho destreinado:

C às vezes é castigado pela sintaxe de suas declarações, particularmente as que envolvem ponteiros para funções. A sintaxe é uma tentativa de fazer a declaração e o uso concordar ; funciona bem para os casos simples, mas pode ser confuso para os mais difíceis, porque as declarações não podem ser lidas da esquerda para a direita e porque os parênteses são usados ​​em excesso. [...]

(Minha ênfase.)

http://codinghighway.com/2013/12/29/the-absolute-definitive-guide-to-decipher-c-declarations/

Então, sim, você está certo, esses são realmente os mesmos operadores aplicados às expressões de declaração, embora como você tenha observado apenas alguns dos operadores façam sentido (por exemplo, ++ não).

Essa estrela é realmente parte do tipo de variável individual que está sendo declarada; no entanto, como outros pôsteres observaram, ele não (mais precisamente, não pode) ser transferido para outras variáveis ​​declaradas ao mesmo tempo (ou seja, na mesma declaração de declaração) - cada variável começa novamente com o mesmo tipo de base que o destino (eventual) e, em seguida, obtém seu próprio (oportunidade de aplicar ou não) ponteiro, matriz e operadores de função para concluir seu tipo.


É por isso que programadores experientes tendem a preferir (1) int *p;vs. int* p;e (2) declarar apenas uma única variável por declaração, mesmo que a linguagem permita mais.

Para adicionar a isso int (*p);uma declaração legal, esses parênteses são simplesmente agrupamentos e, neste caso simples, e não fazem nada diferente int *p. É o mesmo que dizer que uma expressão (não declarativa) (i) >= (0)é a mesma que i >= 0, como você sempre pode adicionar legalmente ().

Eu mencionei que como outro exemplo, embora, é por isso que os programadores preferem uma forma sobre o outro, por isso considero int (*p);vs int(* p);. Talvez você possa ver como o parêntese, a estrela etc. se aplica à variável e não ao tipo base (ou seja, adicionando todos os parênteses permitidos).

Erik Eidt
fonte
1

Nesta declaração de função:

void move(int *units);

o *é parte do tipo, ou seja int*. Não é um operador. É por isso que muitas pessoas escreveriam como:

void move(int* units);
BЈовић
fonte
1

Somente o *, []e ()os operadores têm qualquer significado em declarações (C ++ acrescenta &, mas não vamos entrar em detalhes aqui).

Na declaração

int *p;       

o int-ness de pé especificado pelo especificador de tipo int, enquanto o ponteiro de pé especificado pelo declarador *p .

O tipo de pé "ponteiro para int"; esse tipo é totalmente especificado pela combinação do especificador de tipo intmais o declarador *p.

Em uma declaração, o declarador introduz o nome da coisa que está sendo declarada ( p) junto com informações adicionais sobre o tipo não fornecidas pelo especificador de tipo ("ponteiro para"):

T v;     // v is a single object of type T, for any type T
T *p;    // p is a pointer to T, for any type T
T a[N];  // a is an N-element array of T, for any type T
T f();   // f is a function returning T, for any type T

Isso é importante - ponteiro, matriz e função são especificados como parte do declarador, não como o especificador de tipo 1 . Se você escrever

int* a, b, c;

será analisado como

int (*a), b, c;

portanto, somente aserá declarado como um ponteiro para int; be csão declarados como ints regulares .

A *, []e ()os operadores podem ser combinados para criar tipos arbitrariamente complexas:

T *a[N];      // a is an N-element array of pointers to T
T (*a)[N];    // a is a pointer to an N-element array of T
T *(*f[N])(); // f is an N-element array of pointers to functions 
              // returning pointer to T

T *(*(*(*f)[N])())[M]  // f is a pointer to an N-element array of pointers
                       // to functions returning pointers to M-element
                       // arrays of pointers to T

Observe que *, []e ()obedecem às mesmas regras de precedência em declarações que fazer em expressões. *a[N]é analisado como *(a[N])nas declarações e expressões.

O que é realmente importante perceber é que a forma de uma declaração corresponde à forma da expressão no código. Voltando ao nosso exemplo original, temos um ponteiro para um número inteiro chamado p. Se queremos recuperar esse valor inteiro, usamos o *operador para desreferenciar p, da seguinte forma:

x = *p;

O tipo da expressão *p é intque segue da declaração

int *p;

Da mesma forma, se tivermos uma matriz de ponteiros para doublee desejamos recuperar um valor específico, indexamos na matriz e desreferimos o resultado:

y = *ap[i];

Novamente, o tipo da expressão *ap[i] é double, que segue a declaração

double *ap[N];

Então, por que não ++jogar um papel em uma declaração como *, []ou ()? Ou qualquer outro operador como +ou ->ou &&?

Bem, basicamente, porque a definição da linguagem diz isso. Ele só deixa de lado *, []e ()de desempenhar qualquer papel em uma declaração, uma vez que você tem que ser capaz de especificar ponteiro, matriz e tipos de função. Não há um tipo "increment-this" separado, portanto não há necessidade de ++fazer parte de uma declaração. Não há nenhuma "-este bit a bit" tipo, por isso não há necessidade de unário &, |, ^, ou ~para ser parte de uma declaração de qualquer um. Para tipos que usam o .operador de seleção de membro, usamos as tags structe unionna declaração. Para tipos que usam o ->operador, usamos as tags structe unionem conjunto com o *operador no declarador.


  1. Obviamente, você pode criar nomes de typedef para tipos de ponteiro, matriz e função, como
    typedef int *iptr;
    iptr a,b,c; // all three of a, b, and c are pointers to int
    mas, novamente, é o declarador *iptr que especifica o ponteiro do nome do typedef.

John Bode
fonte
1

O padrão C define apenas dois significados para o *operador:

  • operador indireto
  • operador de multiplicação

É verdade que existem dois significados para o * operador . Não é verdade que esses são os únicos significados do * token .

Em uma declaração como

int *p;

o *não é um operador; faz parte da sintaxe de uma declaração.

A sintaxe da declaração de C visa espelhar sua sintaxe de expressão, para que a declaração acima possa ser lida como " *pé do tipo int", da qual podemos inferir que pé do tipo int*. No entanto, essa não é uma regra firme e não está declarada em nenhum lugar do padrão. Em vez disso, a sintaxe das declarações é definida por si só, separadamente da sintaxe das expressões. É por isso que, por exemplo, a maioria dos operadores que podem aparecer em expressões não pode ter o mesmo significado nas declarações (a menos que façam parte de uma expressão que faz parte da declaração, como o +operador int array[2+2];).

Um caso em que o princípio "declaração espelha o uso" não funciona muito bem é:

int arr[10];

onde arr[10]seria do tipo int se existisse .

E, de fato, existe ainda outro uso do *token. Um parâmetro de matriz pode ser declarado com [*]para indicar uma matriz de comprimento variável. (Isso foi adicionado em C99.) Não *é uma multiplicação nem uma desreferência. Eu suspeito que foi inspirado pela sintaxe do curinga do shell.

Keith Thompson
fonte
Um lugar maior que o conceito "declaração segue o uso" é dividido com a inicialização do ponteiro. O efeito de uma declaração int *p = 0;é semelhante ao uso * p = 0; `mas os significados são muito diferentes.
Supercat
@ supercat: Claro. a inicialização sempre corresponde ao tipo do objeto (neste caso int*).
Keith Thompson