Quais são os comportamentos indefinidos/não especificados comuns para C que você encontra?[fechado]

StackOverflow https://stackoverflow.com/questions/98340

  •  01-07-2019
  •  | 
  •  

Pergunta

Um exemplo de comportamento não especificado na linguagem C é a ordem de avaliação dos argumentos de uma função.Pode ser da esquerda para a direita ou da direita para a esquerda, você simplesmente não sabe.Isso afetaria o modo como foo(c++, c) ou foo(++c, c) é avaliado.

Que outro comportamento não especificado pode surpreender o programador desatento?

Foi útil?

Solução

Uma pergunta de um advogado linguístico.Ok.

Meu top3 pessoal:

  1. violando a regra estrita de alias
  2. violando a regra estrita de alias
  3. violando a regra estrita de alias

    :-)

Editar Aqui está um pequeno exemplo que dá errado duas vezes:

(suponha entradas de 32 bits e little endian)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}

Esse código tenta obter o valor absoluto de um float girando os bits com o bit de sinal diretamente na representação de um float.

No entanto, o resultado da criação de um ponteiro para um objeto convertendo de um tipo para outro não é C válido.O compilador pode assumir que ponteiros para tipos diferentes não apontam para o mesmo pedaço de memória.Isso é verdade para todos os tipos de ponteiros, exceto void* e char* (a sinalização não importa).

No caso acima, faço isso duas vezes.Uma vez para obter um alias interno para o float a e uma vez para converter o valor novamente para float.

Existem três maneiras válidas de fazer o mesmo.

Use um ponteiro char ou void durante a conversão.Eles sempre se referem a qualquer coisa, portanto são seguros.

float funky_float_abs (float a)
{
  float temp_float = a;
  // valid, because it's a char pointer. These are special.
  unsigned char * temp = (unsigned char *)&temp_float;
  temp[3] &= 0x7f;
  return temp_float;
}

Use memcopy.Memcpy usa ponteiros nulos, portanto também forçará o alias.

float funky_float_abs (float a)
{
  int i;
  float result;
  memcpy (&i, &a, sizeof (int));
  i &= 0x7fffffff;
  memcpy (&result, &i, sizeof (int));
  return result;
}

A terceira maneira válida:usar sindicatos.Isto é explicitamente não indefinido desde C99:

float funky_float_abs (float a)
{
  union 
  {
     unsigned int i;
     float f;
  } cast_helper;

  cast_helper.f = a;
  cast_helper.i &= 0x7fffffff;
  return cast_helper.f;
}

Outras dicas

Meu comportamento indefinido favorito é que, se um arquivo de origem não vazio não terminar em uma nova linha, o comportamento será indefinido.

Suspeito que seja verdade que nenhum compilador que jamais verei tratou um arquivo de origem de maneira diferente, de acordo com se ele é ou não terminado ou não, além de emitir um aviso. Portanto, não é realmente algo que surpreenderá os programadores inconscientes, além disso, eles podem se surpreender com o aviso.

Portanto, para problemas de portabilidade genuínos (que principalmente são dependentes da implementação, e não não especificados ou indefinidos, mas acho que isso se enquadra no espírito da pergunta):

  • Char não é necessariamente (un) assinado.
  • INT pode ter qualquer tamanho de 16 bits.
  • Os carros alegóricos não são necessariamente formatados por IEEE ou conforme.
  • Os tipos de número inteiro não são necessariamente o complemento de dois, e o transbordamento aritmético inteiro causa comportamento indefinido (o hardware moderno não trava, mas algumas otimizações do compilador resultarão em comportamento diferente do envoltório, embora seja isso que o hardware faz. Por exemplo, if (x+1 < x) pode ser otimizado como sempre falso quando x Tipo assinado: veja -fstrict-overflow opção no gcc).
  • "/", "." e ".." em um #include não têm significado definido e podem ser tratados de maneira diferente por diferentes compiladores (isso realmente varia e, se der errado, arruinará o seu dia).

Realmente sérios que podem ser surpreendentes mesmo na plataforma que você desenvolveu, porque o comportamento é apenas parcialmente indefinido / não especificado:

  • Posix Threading e o modelo de memória ANSI. O acesso simultâneo à memória não é tão bem definido quanto os novatos pensam. Volátil não faz o que os novatos pensam. A Ordem of Memory Accesses não é tão bem definida quanto os novatos pensam. Acessos posso ser movido através de barreiras de memória em certas direções. A coerência do cache de memória não é necessária.

  • O código de perfil não é tão fácil quanto você pensa. Se o seu loop de teste não tiver efeito, o compilador poderá remover a peça ou tudo. Em linha, não tem efeito definido.

E, como eu acho que Nils mencionou de passagem:

  • Violar a regra estrita de alias.

Dividindo algo por um ponteiro para algo. Só não vou compilar por algum motivo ... :-)

result = x/*y;

O meu favorito é o seguinte:

// what does this do?
x = x++;

Para responder a alguns comentários, é um comportamento indefinido de acordo com o padrão. Vendo isso, o compilador pode fazer qualquer coisa e incluir o formato do seu disco rígido. Veja, por exemplo este comentário aqui. O ponto não é que você possa ver que há uma possível expectativa razoável de algum comportamento. Devido ao padrão C ++ e à maneira como os pontos de sequência são definidos, essa linha de código é realmente um comportamento indefinido.

Por exemplo, se tivéssemos x = 1 Antes da linha acima, qual seria o resultado válido posteriormente? Alguém comentou que deveria ser

x é incrementado por 1

Então, devemos ver x == 2 depois. No entanto, isso não é verdade, você encontrará alguns compiladores que têm x == 1 depois, ou talvez até x == 3. Você teria que olhar atentamente para a assembléia gerada para ver por que isso pode ser, mas as diferenças são devidas para o problema subjacente. Essencialmente, acho que isso ocorre porque o compilador tem permissão para avaliar as duas declarações de tarefas em qualquer ordem que goste, para que possa fazer o x++ Primeiro, ou o x = primeiro.

Outra questão que encontrei (que é definida, mas definitivamente inesperada).

char é mau.

  • assinado ou não assinado, dependendo do que o compilador sente
  • não mandatado como 8 bits

Não posso contar o número de vezes que corrigi os especificadores de formato PrintF para corresponder ao argumento deles. Qualquer incompatibilidade é um comportamento indefinido.

  • Não, você não deve passar um int (ou long) para %x - um unsigned int É necessário
  • Não, você não deve passar um unsigned int para %d - um int É necessário
  • Não, você não deve passar um size_t para %u ou %d - usar %zu
  • Não, você não deve imprimir um ponteiro com %d ou %x - usar %p e lançado para um void *

Um compilador não precisa dizer que você está chamando uma função com o número errado de parâmetros/tipos de parâmetros errados se o protótipo de função não estiver disponível.

Eu já vi muitos programadores relativamente inexperientes mordidos por constantes de vários caracteres.

Este:

"x"

é um literal de corda (que é do tipo char[2] e decai para char* na maioria dos contextos).

Este:

'x'

é uma constante de caráter comum (que, por razões históricas, é do tipo int).

Este:

'xy'

também é uma constante de caráter perfeitamente legal, mas seu valor (que ainda é do tipo int) é definido pela implementação. É um recurso de linguagem quase inútil que serve principalmente para causar confusão.

Os desenvolvedores de Clang postaram alguns Ótimos exemplos Há um tempo, em um post, todo programador C deve ler. Alguns interessantes não mencionados antes:

  • Overflow inteiro assinado - não, não é bom envolver uma variável assinada após o máximo.
  • Desreferenciando um ponteiro nulo - sim, isso é indefinido e pode ser ignorado, consulte a Parte 2 do link.

O EE aqui acabou de descobrir que A >>-2 está um pouco cheio.

Eu assenti e disse a eles que não era natural.

Certifique-se de sempre inicializar suas variáveis ​​antes de usá-las!Quando comecei com C, isso me causou muitas dores de cabeça.

Usando as versões macro de funções como "max" ou "isupper". As macros avaliam seus argumentos duas vezes, para que você tenha efeitos colaterais inesperados quando ligar para max (++ i, j) ou isupper (*p ++)

O exposto acima é para o padrão C. em C ++, esses problemas desapareceram amplamente. A função Max agora é uma função modelada.

esquecendo de adicionar static float foo(); no arquivo de cabeçalho, apenas para obter exceções de ponto flutuante sendo jogadas quando retornaria 0,0F;

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top