É possível inicializar um ponteiro C para NULL?

90

Eu tenho escrito coisas como

char *x=NULL;

na suposição de que

 char *x=2;

criaria um charponteiro para o endereço 2.

Mas, no Tutorial de programação GNU C diz que int *my_int_ptr = 2;armazena o valor inteiro2 em qualquer endereço aleatório que esteja my_int_ptrquando for alocado.

Isso parece implicar que o meu próprio char *x=NULLestá atribuindo qualquer valor deNULL elenco a chara algum endereço aleatório na memória.

Enquanto

#include <stdlib.h>
#include <stdio.h>

int main()
{
    char *x=NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

na verdade, imprime

é nulo

quando eu o compilo e executo, fico preocupado em estar contando com um comportamento indefinido, ou pelo menos um comportamento subespecificado, e que devo escrever

char *x;
x=NULL;

em vez de.

fagricipni
fonte
72
Há uma diferença muito confusa entre o que int *x = whatever;faz e o que int *x; *x = whatever;faz. int *x = whatever;realmente se comporta como int *x; x = whatever;, não *x = whatever;.
user2357112 suporta Monica
78
Este tutorial parece ter entendido errado essa distinção confusa.
user2357112 suporta Monica,
51
Tantos tutoriais de merda na web! Pare de ler imediatamente. Nós realmente precisamos de uma lista negra ASSIM onde possamos envergonhar publicamente livros ruins ...
Lundin
9
@MM O que não torna as coisas menos ruins no ano de 2017. Dada a evolução dos compiladores e computadores desde os anos 80, é basicamente a mesma coisa que se eu fosse um médico e lesse livros de medicina escritos durante o século 18.
Lundin,
13
Não acho que este tutorial se qualifique como " O Tutorial de Programação GNU C" ...
marcelm

Respostas:

114

É possível inicializar um ponteiro C para NULL?

TL; DR Sim, muito.


A afirmação real feita no guia parece

Por outro lado, se você usar apenas a única atribuição inicial,, int *my_int_ptr = 2;o programa tentará preencher o conteúdo do local da memória apontado por my_int_ptrcom o valor 2. Como my_int_ptrestá cheio de lixo, pode ser qualquer endereço. [...]

Bem, eles estão errados, você está certo.

Para a declaração, ( ignorando, por enquanto, o fato de que o ponteiro para a conversão de inteiro é um comportamento definido pela implementação )

int * my_int_ptr = 2;

my_int_ptré uma variável (do tipo ponteiro para int), tem um endereço próprio (tipo: endereço do ponteiro para inteiro), você está armazenando um valor de 2em que endereço.

Agora, my_int_ptrsendo um tipo de ponteiro, podemos dizer que ele aponta para o valor de "tipo" no local da memória apontado pelo valor mantido my_int_ptr. Então, você está essencialmente atribuindo o valor de variável do ponteiro, não o valor da localização da memória apontada pelo ponteiro.

Então, para conclusão

 char *x=NULL;

inicializa a variável de ponteiro xpara NULL, não o valor no endereço de memória apontado pelo ponteiro .

Este é o mesmo que

 char *x;
 x = NULL;    

Expansão:

Agora, sendo estritamente conforme, uma declaração como

 int * my_int_ptr = 2;

é ilegal, pois envolve violação de restrição. Para ser claro,

  • my_int_ptr é uma variável de ponteiro, tipo int *
  • uma constante inteira, 2tem tipo int, por definição.

e eles não são tipos "compatíveis", então esta inicialização é inválida porque está violando as regras de atribuição simples, mencionadas no capítulo §6.5.16.1 / P1, descrito na resposta de Lundin .

Caso alguém esteja interessado em como a inicialização está ligada a restrições de atribuição simples, citando C11, capítulo §6.7.9, P11

O inicializador para um escalar deve ser uma única expressão, opcionalmente entre colchetes. O valor inicial do objeto é o da expressão (após a conversão); aplicam-se as mesmas restrições e conversões de tipo para atribuição simples, considerando o tipo do escalar como a versão não qualificada de seu tipo declarado.

Sourav Ghosh
fonte
@ Random832n Eles estão errados. Eu citei a parte relacionada em minha resposta, corrija-me se não. Ah, e a ênfase em intencional.
Sourav Ghosh
"... é ilegal, pois envolve violação de restrição. ... um literal inteiro, 2 tem tipo int, por definição." é problemático. Parece que porque 2é um int, a atribuição é um problema. Mas é mais do que isso. NULLtambém pode ser um int, um int 0. É só que char *x = 0;está bem definido e char *x = 2;não está. 6.3.2.3 Ponteiros 3 (BTW: C não define um literal inteiro , apenas literal de string e literal composto . 0É uma constante inteira )
chux - Reintegrar Monica
@chux Você está muito correto, mas não é char *x = (void *)0;, em se conformar? ou é apenas com outras expressões que produz o valor 0?
Sourav Ghosh
10
@SouravGhosh: constantes inteiras com valor 0são especiais: elas convertem implicitamente em ponteiros nulos separadamente das regras usuais para converter explicitamente expressões inteiras gerais para tipos de ponteiro.
Steve Jessop
1
A linguagem descrita pelo Manual de Referência C 1974 não permitia declarações para especificar expressões de inicialização, e a falta de tais expressões torna o "uso de espelhos de declaração" muito mais prático. A sintaxe int *p = somePtrExpressioné IMHO um tanto horrível, pois parece que está definindo o valor de, *pmas na verdade está definindo o valor de p.
supercat de
53

O tutorial está errado. No ISO C, int *my_int_ptr = 2;é um erro. No GNU C, significa o mesmo que int *my_int_ptr = (int *)2;. Isso converte o inteiro 2em um endereço de memória, de alguma forma conforme determinado pelo compilador.

Ele não tenta armazenar nada no local endereçado por esse endereço (se houver). Se você continuasse a escrever *my_int_ptr = 5;, ele tentaria armazenar o número 5no local endereçado por esse endereço.

MILÍMETROS
fonte
1
Eu não sabia que a conversão de inteiro para ponteiro é definida pela implementação. Obrigado pela informação.
taskinoor
1
@taskinoor Por favor, note que há uma conversão apenas no caso de você forçar por um elenco, como nesta resposta. Se não for pelo elenco, o código não deve ser compilado.
Lundin,
2
@taskinoor: Sim, as várias conversões em C são bastante confusas. Este Q tem informações interessantes sobre conversões: C: Quando a conversão entre tipos de ponteiro não é um comportamento indefinido? .
sleske
17

Para esclarecer porque o tutorial está errado, int *my_int_ptr = 2;é uma "violação de restrição", é um código que não tem permissão para compilar e o compilador deve fornecer um diagnóstico ao encontrá-lo.

Conforme 6.5.16.1 Atribuição simples:

Restrições

Um dos seguintes deve conter:

  • o operando esquerdo tem tipo aritmético atômico, qualificado ou não qualificado, e o direito tem tipo aritmético;
  • o operando esquerdo tem uma versão atômica, qualificada ou não qualificada de uma estrutura ou tipo de união compatível com o tipo da direita;
  • o operando esquerdo tem tipo de ponteiro atômico, qualificado ou não qualificado, e (considerando o tipo que o operando esquerdo teria após a conversão de lvalue) ambos os operandos são ponteiros para versões qualificadas ou não qualificadas de tipos compatíveis, e o tipo apontado pela esquerda tem todos os qualificadores do tipo apontado pela direita;
  • o operando esquerdo tem tipo de ponteiro atômico, qualificado ou não qualificado, e (considerando o tipo que o operando esquerdo teria após a conversão de lvalue) um operando é um ponteiro para um tipo de objeto e o outro é um ponteiro para uma versão qualificada ou não qualificada de void, e o tipo apontado pela esquerda tem todos os qualificadores do tipo apontado pela direita;
  • o operando esquerdo é um ponteiro atômico, qualificado ou não qualificado, e o direito é uma constante de ponteiro nula; ou
  • o operando esquerdo possui o tipo atômico, qualificado ou não qualificado _Bool, e o direito é um ponteiro.

Nesse caso, o operando esquerdo é um ponteiro não qualificado. Em nenhum lugar ele menciona que o operando correto pode ser um número inteiro (tipo aritmético). Portanto, o código viola o padrão C.

O GCC é conhecido por se comportar mal, a menos que você diga explicitamente que ele é um compilador C padrão. Se você compilar o código como -std=c11 -pedantic-errors, ele dará um diagnóstico correto como deve ser feito.

Lundin
fonte
4
upvoted para sugerir erros -pedantic. Embora provavelmente usarei o relacionado -Wpedantic.
fagricipni
2
Uma exceção à sua declaração de que o operando correto não pode ser um inteiro: a Seção 6.3.2.3 diz: “Uma expressão constante de inteiro com o valor 0, ou tal expressão convertida em tipo void *, é chamada de constante de ponteiro nula”. Observe o penúltimo ponto em sua cotação. Portanto, int* p = 0;é uma forma legal de escrever int* p = NULL;. Embora o último seja mais claro e convencional.
Davislor de
1
O que torna a ofuscação patológica int m = 1, n = 2 * 2, * p = 1 - 1, q = 2 - 1;legal também.
Davislor de
@Davislor que é coberto pelo ponto 5 na citação padrão nesta resposta (concorde que o resumo posterior provavelmente deve mencioná-lo)
MM
1
@chux Eu acredito que um programa bem formado precisaria converter um intptr_texplicitamente para um dos tipos permitidos no lado direito. Ou seja, void* a = (void*)(intptr_t)b;é válido no ponto 4, mas (intptr_t)bnão é um tipo de ponteiro compatível, nem a void*, nem uma constante de ponteiro nula e void* anão é um tipo aritmético nem _Bool. O padrão diz que a conversão é legal, mas não que seja implícita.
Davislor
15

int *my_int_ptr = 2

armazena o valor inteiro 2 em qualquer endereço aleatório que esteja em my_int_ptr quando for alocado.

Isso está completamente errado. Se isto estiver realmente escrito, por favor, obtenha um livro ou tutorial melhor.

int *my_int_ptr = 2define um ponteiro inteiro que aponta para o endereço 2. Você provavelmente terá uma falha se tentar acessar o endereço 2.

*my_int_ptr = 2, ou seja, sem o intna linha, armazena o valor dois em qualquer endereço aleatório para o qual my_int_ptresteja apontando. Tendo dito isso, você pode atribuir NULLa um ponteiro quando ele estiver definido.char *x=NULL;é perfeitamente válido C.

Edit: Enquanto escrevia isso, eu não sabia que a conversão de inteiro para ponteiro é um comportamento definido pela implementação. Por favor, veja as boas respostas de @MM e @SouravGhosh para detalhes.

Taskinoor
fonte
1
É totalmente errado porque é uma violação de restrição, e não por qualquer outro motivo. Em particular, isso é incorreto: "int * my_int_ptr = 2 define um ponteiro inteiro que aponta para o endereço 2".
Lundin
@Lundin: Sua frase "não por qualquer outro motivo" é ela mesma errada e enganosa. Se você corrigir o problema de compatibilidade de tipo, ainda ficará com o fato de que o autor do tutorial está deturpando grosseiramente como as inicializações e atribuições de ponteiro funcionam.
Lightness Races in Orbit
14

Muita confusão sobre os ponteiros C vem de uma escolha muito ruim que foi feita originalmente em relação ao estilo de codificação, corroborada por uma pequena escolha muito ruim na sintaxe da linguagem.

int *x = NULL;está correto C, mas é muito enganoso, eu diria até sem sentido, e tem dificultado o entendimento da língua para muitos novatos. Isso nos faz pensar que mais tarde poderíamos fazer o *x = NULL;que é obviamente impossível. Veja, o tipo da variável não é int, e o nome da variável não *x, nem o *na declaração desempenha qualquer papel funcional em colaboração com o =. É puramente declarativo. Então, o que faz muito mais sentido é o seguinte:

int* x = NULL;que também é C correto, embora não adira ao estilo de codificação original K&R. Isso torna perfeitamente claro que o tipo é int*, e a variável de ponteiro é x, portanto, torna-se evidente até mesmo para os não iniciados que o valor NULLestá sendo armazenado em x, que é um ponteiro para int.

Além disso, torna mais fácil derivar uma regra: quando a estrela está fora do nome da variável, então é uma declaração, enquanto a estrela sendo anexada ao nome é a desreferenciação do ponteiro.

Então, agora fica muito mais compreensível que mais adiante podemos fazer x = NULL;ou, *x = 2;em outras palavras, torna mais fácil para um novato ver como variable = expressionleva a pointer-type variable = pointer-expressione dereferenced-pointer-variable = expression. (Para os iniciados, por 'expressão' quero dizer 'rvalue'.)

A escolha infeliz na sintaxe da linguagem é que, ao declarar variáveis ​​locais, você pode dizer o int i, *p;que declara um inteiro e um ponteiro para um inteiro, o que leva a crer que o *é uma parte útil do nome. Mas não é, e essa sintaxe é apenas um caso especial peculiar, adicionado por conveniência e, em minha opinião, nunca deveria ter existido, porque invalida a regra que propus acima. Pelo que eu sei, em nenhum outro lugar da linguagem esta sintaxe é significativa, mas mesmo se for, ela aponta para uma discrepância na maneira como os tipos de ponteiro são definidos em C. Em todos os outros lugares, em declarações de variável única, em listas de parâmetros, em membros de estrutura, etc., você pode declarar seus ponteiros como em type* pointer-variablevez de type *pointer-variable; é perfeitamente legal e faz mais sentido.

Mike Nakis
fonte
int *x = NULL; is correct C, but it is very misleading, I would even say nonsensical,... Eu tenho que concordar para discordar. It makes one think.... pare de pensar, leia um livro C primeiro, sem ofensa.
Sourav Ghosh de
^^ isso teria feito todo o sentido para mim. Então, suponho que seja subjetivo.
Mike Nakis,
5
@SouravGhosh Por uma questão de opinião acho que C deveria ter sido projetado de forma que int* somePtr, someotherPtrdeclare duas dicas, na verdade, eu costumava escrever, int* somePtrmas isso leva ao bug que você descreve.
fagricipni de
1
@fagricipni Eu parei de usar a sintaxe de declaração de múltiplas variáveis ​​por causa disso. Eu declaro minhas variáveis ​​uma por uma. Se eu realmente quiser que eles estejam na mesma linha, eu os separo com ponto e vírgula em vez de vírgulas. "Se um lugar é ruim, não vá para aquele lugar."
Mike Nakis,
2
@fagricipni Bem, se eu pudesse ter projetado o Linux do zero, teria usado em createvez de creat. :) A questão é, é como é e precisamos nos moldar para nos adaptar a isso. Tudo se resume a escolha pessoal no final do dia, concordo.
Sourav Ghosh
6

Eu gostaria de acrescentar algo ortogonal às muitas respostas excelentes. Na verdade, inicializar em NULLestá longe de ser uma prática ruim e pode ser útil se esse ponteiro puder ou não ser usado para armazenar um bloco de memória alocado dinamicamente.

int * p = NULL;
...
if (...) {
    p = (int*) malloc(...);
    ...
}
...
free(p);

Visto que de acordo com o padrão ISO-IEC 9899 free é um nop quando o argumento é NULL, o código acima (ou algo mais significativo na mesma linha) é legítimo.

Luca Citi
fonte
5
É redundante converter o resultado de malloc em C, a menos que o código C também deva ser compilado como C ++.
gato
Você está certo, o void*é convertido conforme necessário. Mas ter um código que funciona com um compilador C e C ++ pode trazer benefícios.
Luca Citi,
1
@LucaCiti C e C ++ são linguagens diferentes. Só haverá erros esperando por você se você tentar compilar um arquivo-fonte escrito para um usando um compilador projetado para o outro. É como tentar escrever código C que você possa compilar usando ferramentas Pascal.
Evil Dog Pie
1
Bom conselho. Eu (tento) sempre inicializar minhas constantes de ponteiro para algo. Em C moderno, esse geralmente pode ser seu valor final e eles podem ser constponteiros declarados em res medias , mas mesmo quando um ponteiro precisa ser mutável (como um usado em um loop ou por realloc()), configurando-o para NULLdetectar bugs onde ele foi usado antes é definido com seu valor real. Na maioria dos sistemas, o desreferenciamento NULLcausa um segfault no ponto de falha (embora haja exceções), enquanto um ponteiro não inicializado contém lixo e a gravação nele corrompe a memória arbitrária.
Davislor
1
Além disso, é muito fácil ver no depurador que um ponteiro contém NULL, mas pode ser muito difícil diferenciar um ponteiro lixo de um válido. Portanto, é útil garantir que todos os ponteiros sejam sempre válidos ou NULL, desde o momento da declaração.
Davislor
1

este é um ponteiro nulo

int * nullPtr = (void*) 0;
Ahmed Nabil El-Gawahergy
fonte
1
Isso responde ao título, mas não ao corpo da pergunta.
Fabio diz Restabelecer Monica em
1

Isto está certo.

int main()
{
    char * x = NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

Esta função é correta para o que faz. Ele atribui o endereço de 0 ao ponteiro char x. Ou seja, ele aponta o ponteiro x para o endereço de memória 0.

Alternativo:

int main()
{
    char* x = 0;

    if ( !x )
        printf(" x points to NULL\n");

    return EXIT_SUCCESS;
}

Meu palpite sobre o que você queria é:

int main()
{
    char* x = NULL;
    x = alloc( sizeof( char ));
    *x = '2';

    if ( *x == '2' )
        printf(" x points to an address/location that contains a '2' \n");

    return EXIT_SUCCESS;
}

x is the street address of a house. *x examines the contents of that house.
Vanderdecken
fonte
"Ele atribui o endereço de 0 ao ponteiro char x." -> Talvez. C não especifica o valor do ponteiro, apenas isso char* x = 0; if (x == 0)será verdadeiro. Os ponteiros não são necessariamente inteiros.
chux - Reintegrar Monica de
Ele não 'aponta o ponteiro x para o endereço de memória 0'. Ele define o valor do ponteiro como um valor inválido não especificado que pode ser testado comparando-o com 0 ou NULL. A operação real é definida pela implementação. Não há nada aqui que responda à pergunta real.
Marquês de Lorne