Quiz da linguagem de programação C
(stefansf.de)- As regras da linguagem C podem fazer até códigos que parecem simples, como comparação de ponteiros, aliasing, ponteiro nulo e valores não inicializados, se tornarem comportamento indefinido
- Constantes inteiras,
sizeof, constantes de caractere e aritmética comuint8_tpodem produzir resultados diferentes dependendo da plataforma, da notação e do ponto de atribuição intermediária por causa da escolha de tipos e das promoções inteiras - Em declarações de função,
foo()efoo(void), a ausência de protótipo, as promoções padrão de argumentos e funções sem valor de retorno fazem a legalidade e o comportamento diferirem entre C e C++ - Arrays não são ponteiros, parâmetros de array são ajustados para ponteiros, e
a,&ae&a[0], embora tenham o mesmo endereço, têm tipos diferentes, então não podem ser usados de forma intercambiável - Precedência de operadores e ordem de avaliação são coisas distintas e, incluindo a estrutura do corpo de
switche até o tempo de vida de objetos temporários, a redação da norma determina o resultado real da execução
Comportamento indefinido e regras de ponteiros
-
Comparação de ponteiros e regra de aliasing estrito
- Mesmo que ponteiros
peqdo mesmo tipo apontem para o mesmo endereço, se eles se originaram de objetos diferentes e não fazem parte do mesmo objeto aggregate ou union, a comparaçãop == qpode ser comportamento indefinido - A ideia de que ponteiros são mais abstratos do que simples endereços numéricos é desenvolvida neste artigo relacionado
- Se um objeto
intfor acessado por meio de um lvalueshort, isso se torna comportamento indefinido pela regra de strict aliasing - Um ponteiro
unsigned charé uma exceção e pode fazer alias de qualquer objeto, então acessar um objetointpor meio de um lvalueunsigned charé legal - Há garantia de que
unsigned charnão tem bits de padding nem trap representation, e desde C11 também há garantia de quesigned charnão tem bits de padding - A análise de aliasing baseada em tipo é abordada neste artigo relacionado
- Mesmo que ponteiros
-
Ponteiro nulo e representação de ponteiros
- A representação em bits de um ponteiro nulo não precisa necessariamente ser todos os bits em 0
- O padrão C define uma null pointer constant, mas não define a representação de um ponteiro nulo em tempo de execução nem a representação de ponteiros em geral
- O Symbolics Lisp Machine 3600 usa uma tupla no formato
<array-object, index>em vez de ponteiros numéricos, e a representação de ponteiro nulo é<nil, 0> - Há exemplos adicionais no clc FAQ 5.17
- A constante
0se torna inteiro ou ponteiro nulo dependendo do contexto, e(void *)0é avaliado como ponteiro nulo - O fato de uma expressão
eser avaliada como0não garante que(void *)eserá um ponteiro nulo - Somente quando uma null pointer constant é convertida para um tipo de ponteiro há garantia de equivalência a um ponteiro nulo
- A aritmética com ponteiro nulo é comportamento indefinido, então mesmo que
eseja um ponteiro nulo, não há garantia de quee + 0continue sendo um ponteiro nulo
-
Valores não inicializados
- Ao ler um objeto com duração de armazenamento automática não inicializado, se esse objeto puder ter a classe de armazenamento
registere seu endereço nunca tiver sido obtido, isso será comportamento indefinido conforme C11 § 6.3.2.1 ¶ 2 - Essa regra se conecta à arquitetura Intel Itanium tratada em DR338
- Os registradores inteiros gerais do Itanium têm 64 bits e um trap bit, e esse trap bit é o
NaT(not-a-thing), que indica se o registrador foi inicializado - Se o endereço da variável for obtido, essa condição desaparece, mas o valor continua indeterminado e pode ser uma trap representation ou um valor não especificado
- Ler uma trap representation é comportamento indefinido segundo C11 § 6.2.6.1 ¶ 5
- Se for um valor não especificado, até o resultado de
x != xpode sertrueoufalse, e seint xfor não especificado, não há garantia de quexserá 0 mesmo apósx *= 0 - Valores indeterminados e valores não especificados são discutidos em DR260, DR451, N1793, N1818, N2012, N2013, N2221
- Ao ler um objeto com duração de armazenamento automática não inicializado, se esse objeto puder ter a classe de armazenamento
-
unsigned charememcpy- O tipo
unsigned charnão tem trap representation segundo C11 § 6.2.6.1 ¶ 3, então o valor inicial é não especificado - Uma resposta de um membro do comitê de C no StackOverflow afirma que, após a chamada da função da biblioteca padrão
memcpy, o valor dexdeve se tornar especificado, e nessa interpretaçãox != xseriafalse - A base no padrão C para sustentar isso não é clara, e a resposta do comitê em DR451 afirma que usar uma função de biblioteca com um valor indeterminado é comportamento indefinido, o que entra em conflito com essa interpretação
- Essa questão continua em aberto, e há discussão adicional em Uninitialized Reads
- O tipo
Constantes inteiras, promoção e sizeof
-
Notação e tipo de constantes inteiras
- Constantes inteiras decimais sem sufixo são sempre escolhidas a partir da lista de tipos signed, mas constantes octais e hexadecimais podem se tornar tipos signed ou unsigned
- De acordo com a C17 § 6.4.4.1, o tipo de uma constante inteira é definido como o primeiro tipo da lista capaz de representar aquele valor
- Sem sufixo, constantes decimais seguem a ordem
int,long int,long long int, enquanto constantes octais e hexadecimais seguemint,unsigned int,long int,unsigned long int,long long int,unsigned long long int - Constantes entre
INT_MAX+1eUINT_MAXpodem ter tipos diferentes dependendo de serem decimais ou hexadecimais, e isso pode causar diferenças em código sensível à ABI, como chamadas de funções variádicas - Na Arm 32-bit architecture ABI,
intelongsão passados como 32 bits em um registrador, enquantolong longé passado como 64 bits em dois registradores - Em plataformas onde
inttem 32 bits,-1 < 0x8000resulta emtrue; em plataformas ondeinttem 16 bits, resulta emfalse, o que pode gerar problemas de portabilidade - Em expressões como generic selection, funções sobrecarregadas de C++ e
sizeof(0x80000000) == sizeof(2147483648), a diferença de tipo das constantes também pode mudar o resultado
-
sizeof(int) > -1- O operador
sizeofretorna um inteiro unsigned do tiposize_t - De acordo com as usual arithmetic conversions da C11 § 6.3.1.8, se um operando signed tiver rank menor que o operando unsigned, ele será convertido para um tipo unsigned de mesmo rank
- Um inteiro signed correspondente a
-1, ao ser convertido para unsigned, torna-se o maior inteiro unsigned daquele rank - Portanto,
sizeof(int) > -1sempre é avaliado comofalse
- O operador
-
Tipo de constantes de caractere
- Em C, constantes de caractere têm tipo
int, de acordo com a C11 § 6.4.4.4 ¶ 10 - Portanto, não há garantia de que
sizeof(char) == sizeof('x')seja sempretrue; apenassizeof(int) == sizeof('x')é garantido - Uma integer character constant pode ser uma sequência de um ou mais caracteres multibyte, então
'abc'também é válido, e sua representação é definida pela implementação - O valor de uma integer character constant contendo um único caractere é igual à representação inteira de um objeto do tipo
charque representa esse mesmo caractere único
- Em C, constantes de caractere têm tipo
-
Aritmética com
uint8_te divisão- Mesmo que
a,bectenham sido inicializados antes da leitura, os valores dexezpodem ser diferentes por causa da promoção inteira e do ponto onde ocorre a atribuição intermediária - O valor de cada variável é promovido para o tamanho de
int, depois a soma e a divisão são executadas, e cada resultado de atribuição é truncado e armazenado no tipo da variável correspondente - Por exemplo, se
a=255,b=1,c=2, entãoxse torna((255 + 1) / 2) % 256 = 128 - A variável intermediária
yse torna(255 + 1) % 256 = 0, e depoiszse torna(0 / 2) % 256 = 0, portanto128 != 0 - Overflow de inteiro unsigned é um comportamento definido
- Como a operação módulo se distribui sobre a adição, se a divisão for trocada por adição,
xezsempre serão iguais - Se a primeira atribuição for alterada para
uint8_t x = ((uint8_t)(a + b)) / c;,xeztambém sempre serão iguais
- Mesmo que
-
Variáveis
conste variable length array- Mesmo usando as variáveis qualificadas com
constnemcomo tamanho de array, elas não são integer constant expression em C - Na C11 § 6.6 ¶ 6, integer constant expression é limitada a integer constant, enumeration constant, character constant, resultado de
sizeof,_Alignofe cast cujo resultado seja inteiro e cujo operando imediato seja uma floating constant, entre outros casos restritos - Se a expressão de tamanho do array não for uma integer constant expression, então, de acordo com a C11 § 6.7.6.2 ¶ 4, ela se torna uma variable length array
- Variable length array não é permitida em file scope, então a compilation unit com o array global
xnão compila - Em block scope, variable length array é permitida, então a compilation unit com o array local
ypode ser compilada - Variable length array é uma conditional feature cujo suporte pela implementação não é obrigatório, então, em compiladores que não a suportam, até o exemplo em block scope pode não compilar
- Em C++, as duas compilation units compilam, e como C++ não tem o conceito de variable length array,
yé compilado como um array comum com 42 elementos
- Mesmo usando as variáveis qualificadas com
Declaração de função, valor de retorno, linkage
-
foo()efoo(void)- Uma declaração de função na forma
foo()declara uma função cujo número e tipos de argumentos são desconhecidos, enquantofoo(void)declara uma função nulária sem argumentos - Essa diferença é tratada em um texto sobre declarações, definições e protótipos de função
- Como uma declaração sem lista de argumentos apenas introduz o nome da função e não define a quantidade nem os tipos dos argumentos, ela pode ser legal ao se combinar com a definição posterior da função
- Se uma função for chamada sem protótipo, aplicam-se as promoções padrão de argumentos, e
floaté promovido paradouble - Se o tipo da função após a promoção não for compatível com o tipo da definição real da função, a combinação entre declaração e definição não é válida
- Uma chamada de função sem declaração pode ser compilada em C porque funções implícitas podiam ser permitidas, mas em C++ isso é erro de compilação
- Se você fizer uma chamada como
bar(42)sem declaração, as promoções de argumentos inteiros serão aplicadas e42será representado comoint; portanto, sebarnão for compatível comT (*)(int)para algum tipo de retornoT, isso resulta em comportamento indefinido
- Uma declaração de função na forma
-
Função com retorno de valor que não retorna valor
- Mesmo que uma função com tipo de retorno
intnão retorne um valor, isso pode ser legal em C desde que o valor resultante da chamada não seja usado - No K&R C não existia o tipo
void, e quando o tipo era omitido assumia-se o tipo padrãoint; por isso, funções que não retornam valor e a regra deintimplícito têm uma ligação histórica - A regra de
intimplícito foi removida no C99, e a discussão relacionada aparece em N661 e na rationale do C99 - C17 § 6.9.1 ¶ 12 estabelece que, se a execução chegar ao
}no fim da função e o chamador usar o valor da chamada da função, isso é comportamento indefinido - Em C++98 § 6.6.3 ¶ 2, simplesmente alcançar o fim de uma função com retorno de valor equivale a um
returnsem valor, e isso se torna comportamento indefinido em uma função que retorna valor - Como compiladores C++ em geral não conseguem provar que
abort_program()termina a execução em algum ramo, nesses casos eles normalmente só emitem um diagnóstico, em vez de um erro
- Mesmo que uma função com tipo de retorno
-
linkage e
extern- Se o mesmo identificador for redeclarado com
externem um escopo onde uma declaração anterior está visível, então o linkage da declaração posterior será o mesmo da declaração anterior - C17 § 6.2.2 ¶ 4 determina que, se a declaração anterior especificou linkage interno ou externo, então a declaração
externposterior também terá o mesmo linkage - Se a declaração anterior não estiver visível, ou se ela não tiver linkage, o identificador com
externterá linkage externo - Combinações de declarações na ordem inversa podem resultar em comportamento indefinido, e GCC e Clang detectam isso
- Se o mesmo identificador for redeclarado com
Qualificadores e tipos incompletos
-
constem parâmetros de função- Em uma declaração de função, o parâmetro
xpode ser qualificado comconste, na definição da função, não ser; ainda assim, atribuir um valor axdentro do corpo da função é legal - De acordo com C11 § 6.7.6.3 ¶ 15, ao determinar a compatibilidade de tipos de parâmetros de função e o tipo composto, cada parâmetro declarado com tipo qualificado é tratado como sua versão não qualificada
- O mesmo tema também é tratado em DR040
- Em uma declaração de função, o parâmetro
-
constno tipo de retorno da função- Quando apenas o tipo de retorno da definição da função é qualificado com
const, mas a declaração não é, a resposta não é algo que se possa classificar simplesmente como certo ou errado - O consenso geral é que qualificadores em rvalues devem ser ignorados, mas a redação do padrão até o C11 não tratava disso explicitamente
- No C17, ficou claro que qualificadores de rvalue devem ser ignorados em cast, conversão de lvalue e declarador de função
- C17 § 6.7.6.3 ¶ 5 afirma que o tipo retornado pela função é a versão não qualificada de
T, e essa redação foi adicionada no C17 - A atribuição entre tipos de função pode ser legal mesmo que a qualificação
constdo tipo de retorno seja diferente - Discussões adicionais estão em DR423 e DR481
- Quando apenas o tipo de retorno da definição da função é qualificado com
-
Struct incompleta e variável global
- No momento da declaração de uma variável global,
struct foopode ser um tipo incompleto e seu tamanho pode ser desconhecido; ainda assim, isso pode ser permitido em certas situações se o tipo for completado depois, na mesma translation unit - Uma lógica parecida também se aplica a variáveis globais ou arrays de tipo incompleto
- Esse conteúdo também é tratado em DR016
- No momento da declaração de uma variável global,
-
Objeto externo de tipo
void- Uma declaração de variável de tipo
voidcom linkage interno não é legal, mas uma declaração de variável de tipovoidcom linkage externo é sintaticamente legal e não é explicitamente proibida em nenhum ponto do padrão C11 - Segundo C11 § 6.2.5 ¶ 19, o tipo
voidé um tipo de objeto incompleto que não pode ser completado, composto pelo conjunto vazio de valores - C11 § 6.3.2.1 ¶ 1 define lvalue como uma expressão de tipo objeto que não seja
void; portanto, o nome de um objetofoode tipovoidnão é um lvalue válido - Com base no C11, é difícil imaginar operações significativas e conformes sobre um objeto externo de tipo
void - DR012 trata do caso em que o tipo é alterado para
const void, tornando legal obter o endereço do objetofoo, o que parece mais uma falha de supervisão do que um recurso intencional
- Uma declaração de variável de tipo
-
Conversão de ponteiro-
const- Quando
Té um tipo de objeto derivado, a atribuição acpé legal, mas não há uma resposta curta para dizer se a atribuição acppé legal - Esse tema é tratado em um texto sobre conversão implícita para ponteiro para const
- Quando
Arrays, literais de string e ajuste de ponteiros
-
Array não é ponteiro
- Inicialização de array e inicialização de ponteiro não são equivalentes
- A primeira forma inicializa um array modificável com duração de armazenamento automática ou estática
- A segunda forma inicializa um ponteiro que aponta para um array com duração de armazenamento estática, e esse array não é necessariamente modificável
- Array não é ponteiro, e mais detalhes podem ser vistos neste texto relacionado
-
a,&a,&a[0]- Em
int a[42];,a,&ae&a[0]são todos avaliados como o endereço do primeiro elemento do array - No entanto, como os tipos das três expressões são diferentes, elas não podem ser usadas de forma intercambiável
- Mais detalhes estão neste texto relacionado
- Em
-
Parâmetros de array e arrays locais
- Se o tipo de um parâmetro de função for “array de
T”, ele será ajustado para “ponteiro paraT” - Mesmo que o parâmetro
xpareça serint[42], na prática ele é tratado comoint * - Se a variável local
yforint[42], entãosizeof(y)será42 * sizeof(int) - Como, em geral, o tamanho de um ponteiro para objeto não é igual ao tamanho de 42 inteiros,
sizeof(x) == sizeof(y)normalmente éfalse - Mais detalhes estão neste texto relacionado
- Se o tipo de um parâmetro de função for “array de
Operadores, ordem de avaliação, fluxo de controle
-
x+++y- Em C, não é possível definir novos operadores como em C++, então não existe um novo operador como
+++ x+++yé interpretado como uma combinação de operadores existentes e é equivalente a(x++) + y--*--ptambém não é um novo operador, mas uma combinação de operadores existentes--*--pé equivalente a--(*(--p))e, no exemplo, é avaliado como-1, com o efeito colateral de atribuir-1ax[0]
- Em C, não é possível definir novos operadores como em C++, então não existe um novo operador como
-
Ordem de avaliação de operandos aritméticos
- A precedência de operadores é bem definida, mas a ordem de avaliação dos operandos aritméticos não é definida
(x=1) + (x=2)é um comportamento indefinido porque a ordem das duas atribuições não é definida, então o valor final dexnão é determinado como1ou2- Com a opção
-std=c11 -O2, o GCC 8.2.1 avalia a expressão de exemplo como4, enquanto o Clang 7.0.0 a avalia como3
-
Ordem de avaliação de operadores lógicos
- Nos operadores lógicos
&&e||, a ordem de avaliação dos operandos também é bem definida - Na terminologia do padrão C, existe um sequence point entre a avaliação do primeiro operando e a do segundo
- No exemplo, primeiro
x=1é avaliado e resulta emtrue; em seguida,x=2é avaliado e também resulta emtrue, então a expressão inteira resulta emtrue
- Nos operadores lógicos
-
Estrutura livre do corpo de
switch- O corpo de uma instrução
switchpode ser qualquer statement, então uma estrutura misturando loop eiftambém pode ser válida - Mesmo no branch
truede uma instruçãoifcuja expressão de controle é semprefalse, se houver um rótulocase, essa instrução se torna live, eprintf("1");não é dead code - Ao saltar para
case 2, a clause-1 e a expressão de controle do loop podem não ser executadas, portanto a variáveliprecisa estar inicializada previamente - Mesmo que ocorra fall through em
case 1por não haverbreak, secase 1estiver no branchtruedoifecase 2no branchfalse, ele pode pularcase 2e continuar emcase 3 - Após as três chamadas
foo(0); foo(1); foo(2);, a saída no console será02313223 - Um exemplo real famoso que mistura loop e switch é o Duff's device
- O corpo de uma instrução
Diferenças entre versão do padrão C e tempo de vida de objetos temporários
- Um determinado trecho de código é comportamento indefinido em C11, mas pode não ser em C99
- Em C11, o tempo de vida de certos objetos foi reduzido, de modo que o objeto retornado por uma chamada de função sobrevive apenas até durante a avaliação do operando à direita
- Em C99, o mesmo objeto sobrevive até o fim do bloco envolvente
- Referenciar um objeto cujo tempo de vida terminou é comportamento indefinido, de acordo com C11 § 6.2.4 ¶ 2
- Mesmo em C99, o tempo de vida de objetos com automatic storage duration está vinculado ao bloco envolvente mais próximo, então referenciar o objeto fora desse bloco é comportamento indefinido
- C11 § 6.2.4 ¶ 8 especifica que, se uma non-lvalue expression de tipo struct ou union incluir um membro array, ela se refere a um objeto com automatic storage duration e temporary lifetime
- O tempo de vida desse objeto temporário começa quando a expressão é avaliada e termina quando acaba a avaliação da full expression ou do full declarator que o contém
- Tentar modificar um objeto com temporary lifetime é comportamento indefinido
- Esse exemplo foi extraído de N1285, onde também há discussões adicionais
1 comentários
Opiniões no Lobste.rs
A questão 4 não é válida em C23, mas era válida antes
A questão 10 não é nem certa nem errada, então incomoda um pouco chamá-la de múltipla escolha
A questão 15 está tecnicamente errada, especialmente em relação à questão 13, e a questão 20 é “não especificado”, então também não corresponde a nenhuma resposta
A questão 30 é ambígua dependendo da leitura
Ainda assim, acertei 27 de 31, e ser desenvolvedor de compiladores ajudou um pouco
Depois de resolver umas quatro questões, desapareceu a sensação que eu ainda tinha de que C é simples o bastante para valer a pena usar em projetos paralelos
-std=<language-standard>-pedantic -Wall -Wextrano GCC ou noclang, corrigir de fato todo aviso que aparecer e evitar ao máximo casts de ponteiro e manipulação de ponteiros, parece que dá para escapar das grandes armadilhasHoje em dia os avisos do GCC/
clangsão bem bons, e em <language-standard> você pode usar c89, c99, c11 ou c23Se você usar um compilador como o tcc, que não faz otimizações esquisitas, vai ter menos surpresas bizarras
Fui só pelo critério de “qual seria o comportamento mais absurdo aqui?” e acertei 21 de 32
A maioria dos erros aconteceu porque eu não pensei fundo o bastante no nível desse absurdo
Faz mais de 15 anos que só encostei um pouco em C, e ver um quiz desses não me dá vontade de voltar a usar
Pelo padrão do C23, a resposta da questão 4 não é válida
Curiosamente, faz um tempo que não uso C, mas acertei 27 de 32
É por essas e outras que venho dependendo de verificadores estáticos e linters
Já comecei desconfiado desde a questão 1
Não levaram em conta de onde aqueles ponteiros poderiam vir, e o caso mencionado ali exige condições muito específicas para se sustentar
Na maioria dos casos, o próprio ato de tentar criar o ponteiro já é comportamento indefinido, mas ainda assim dá para considerar justo
A questão 3 realmente me surpreendeu, mais uma armadilha de C
O simples fato de literais inteiros em C terem tipo fixo já é extremamente irritante
As regras de promoção de inteiros até ajudam em certa medida, mas também são fonte de erros
Linguagens modernas, em sua maioria — ou talvez todas — deveriam proibir casts numéricos implícitos, inferir o tipo do literal a partir do contexto quando possível e, quando não for, exigir cast explícito
Depois da questão 6, parei porque já não confiava no teste
No começo foi porque a resposta da questão 5 parecia ter sido basicamente projetada para fazer você errar a questão 6, mas revendo agora, a própria questão 6 parece estar errada
A explicação diz que a chamada da função é comportamento indefinido, mas a pergunta era se a definição da função era legal, e provavelmente era
E isso não me parece um caso tão raro assim
A questão de
switch()foi realmente muito boaEra complicada, mas o processo de resolver mentalmente foi muito divertido