Quiz sobre el lenguaje de programación C
(stefansf.de)- Las reglas del lenguaje C pueden hacer que incluso código que parece simple, como la comparación de punteros, aliasing, punteros nulos y valores sin inicializar, se convierta en comportamiento indefinido
- Las constantes enteras y
sizeof, las constantes de carácter y la aritmética deuint8_tpueden dar resultados distintos según la plataforma, la notación y el punto de asignación intermedia debido a la selección de tipos y las promociones enteras foo()yfoo(void)en declaraciones de funciones, la ausencia de prototipos, las promociones predeterminadas de argumentos y las funciones sin valor de retorno difieren en C y C++ en cuanto a legalidad o comportamiento- Los arreglos no son punteros; los parámetros de arreglo se ajustan a punteros; y
a,&ay&a[0], aunque tengan la misma dirección, tienen tipos distintos, por lo que no pueden usarse indistintamente - La precedencia de operadores y el orden de evaluación son cosas distintas y, junto con la estructura del cuerpo de
switche incluso la vida útil de objetos temporales, la redacción del estándar determina el resultado real de la ejecución
Comportamiento indefinido y reglas de punteros
-
Comparación de punteros y regla estricta de aliasing
- Aunque los punteros
pyqdel mismo tipo apunten a la misma dirección, si provienen de objetos distintos y no forman parte del mismo objeto aggregate o union, la comparaciónp == qpuede ser comportamiento indefinido - La idea de que un puntero es más abstracto que una simple dirección numérica continúa en este artículo relacionado
- Si se accede a un objeto
intmediante un lvalueshort, eso se convierte en comportamiento indefinido según la regla de strict aliasing - Un puntero
unsigned charpuede, como excepción, hacer alias de cualquier objeto, por lo que es legal acceder a un objetointmediante un lvalueunsigned char - Se garantiza que
unsigned charno tiene bits de relleno ni trap representation, y desde C11 también se garantiza quesigned charno tiene bits de relleno - El análisis de aliasing basado en tipos se trata en este artículo relacionado
- Aunque los punteros
-
Puntero nulo y representación de punteros
- La representación en bits de un puntero nulo no tiene por qué ser necesariamente todos los bits en 0
- El estándar de C define una null pointer constant, pero no define la representación de un puntero nulo en tiempo de ejecución ni la representación de los punteros en general
- La Symbolics Lisp Machine 3600 usa una tupla con forma
<array-object, index>en lugar de punteros numéricos, y su representación de puntero nulo es<nil, 0> - Hay ejemplos adicionales en clc FAQ 5.17
- La constante
0puede ser un entero o un puntero nulo según el contexto, y(void *)0se evalúa como un puntero nulo - Que una expresión
ese evalúe a0no garantiza que(void *)ese convierta en un puntero nulo - Solo cuando una null pointer constant se convierte a un tipo puntero se garantiza que sea igual a un puntero nulo
- La aritmética sobre un puntero nulo es comportamiento indefinido, por lo que aunque
esea un puntero nulo, no se garantiza quee + 0siga siendo un puntero nulo
-
Valores sin inicializar
- Al leer un objeto de duración de almacenamiento automática sin inicializar, si ese objeto puede ser de clase de almacenamiento
registery nunca se tomó su dirección, entonces según C11 § 6.3.2.1 ¶ 2 eso se convierte en comportamiento indefinido - Esta regla está relacionada con la arquitectura Intel Itanium tratada en DR338
- Los registros enteros generales de Itanium tienen 64 bits y un trap bit, que es
NaT(not-a-thing), para indicar si el registro fue inicializado - Si se toma la dirección de la variable, esa condición desaparece, pero el valor sigue siendo indeterminado y puede ser una trap representation o un unspecified value
- Leer una trap representation produce comportamiento indefinido según C11 § 6.2.6.1 ¶ 5
- Si se trata de un unspecified value, el resultado de
x != xpuede ser tantotruecomofalse, y siint xes unspecified, incluso después dex *= 0no se garantiza quexsea 0 - Los conceptos de indeterminate y unspecified value se discuten en DR260, DR451, N1793, N1818, N2012, N2013, N2221
- Al leer un objeto de duración de almacenamiento automática sin inicializar, si ese objeto puede ser de clase de almacenamiento
-
unsigned charymemcpy- El tipo
unsigned charno tiene trap representation según C11 § 6.2.6.1 ¶ 3, por lo que su valor inicial es unspecified - Una respuesta en StackOverflow de un miembro del comité de C sostiene que, tras llamar a la función de biblioteca estándar
memcpy, el valor dexdebería pasar a ser specified, y bajo esa interpretaciónx != xseríafalse - La base en el estándar de C para respaldar esto no es clara, y la respuesta del comité en DR451 entra en conflicto con esa interpretación al afirmar que usar funciones de biblioteca sobre un indeterminate value es comportamiento indefinido
- Esta cuestión sigue abierta, y hay más discusión en Uninitialized Reads
- El tipo
Constantes enteras, promociones y sizeof
-
Notación y tipo de las constantes enteras
- Las constantes enteras decimales sin sufijo siempre se eligen de la lista de tipos signed, mientras que las constantes octales y hexadecimales pueden ser de tipo signed o unsigned
- Según C17 § 6.4.4.1, el tipo de una constante entera se determina como el primer tipo de la lista que puede representar ese valor
- Cuando no hay sufijo, las constantes decimales siguen el orden
int,long int,long long int; las octales y hexadecimales siguen el ordenint,unsigned int,long int,unsigned long int,long long int,unsigned long long int - Las constantes entre
INT_MAX+1yUINT_MAXpueden tener distinto tipo según sean decimales o hexadecimales, y eso puede generar diferencias en código sensible al ABI, como llamadas a funciones con argumentos variables - En el ABI de arquitectura Arm de 32 bits,
intylongse pasan como 32 bits en un solo registro, mientras quelong longse pasa como 64 bits en dos registros - En plataformas donde
intes de 32 bits,-1 < 0x8000datrue, y en plataformas dondeintes de 16 bits dafalse, lo que puede generar problemas de portabilidad - La diferencia de tipos de las constantes también puede cambiar el resultado en expresiones como generic selection, funciones sobrecargadas de C++ y
sizeof(0x80000000) == sizeof(2147483648)
-
sizeof(int) > -1- El operador
sizeofdevuelve un entero unsigned de tiposize_t - Según las usual arithmetic conversions de C11 § 6.3.1.8, si el operando signed tiene menor rank que el operando unsigned, se convierte al tipo unsigned del mismo rank
- El entero signed correspondiente a
-1, al convertirse a unsigned, pasa a ser el mayor entero unsigned de ese rank - Por lo tanto,
sizeof(int) > -1siempre se evalúa comofalse
- El operador
-
Tipo de las constantes de carácter
- En C, una constante de carácter es de tipo
intsegún C11 § 6.4.4.4 ¶ 10 - Por lo tanto, no hay garantía de que
sizeof(char) == sizeof('x')sea siempretrue; solo está garantizadosizeof(int) == sizeof('x') - Una integer character constant puede ser una secuencia de uno o más caracteres multibyte, así que
'abc'también es válido, y su representación está definida por la implementación - El valor de una integer character constant que contiene un solo carácter es igual a la representación entera de un objeto de tipo
charque representa ese mismo carácter único
- En C, una constante de carácter es de tipo
-
Aritmética con
uint8_ty división- Aunque
a,bycestén inicializadas antes de leerse, los valores dexyzpueden diferir debido a la promoción de enteros y al lugar donde ocurre la asignación intermedia - El valor de cada variable se promociona al tamaño de
intantes de realizar la suma y la división, y cada resultado asignado se trunca y se guarda en el tipo de la variable correspondiente - Por ejemplo, si
a=255,b=1,c=2, entoncesxserá((255 + 1) / 2) % 256 = 128 - La variable intermedia
yserá(255 + 1) % 256 = 0, y luegozserá(0 / 2) % 256 = 0, así que128 != 0 - El overflow de enteros unsigned es un comportamiento definido
- Como la operación módulo se distribuye sobre la suma, si se reemplaza la división por una suma,
xyzsiempre serán iguales - Si la primera asignación se cambia a
uint8_t x = ((uint8_t)(a + b)) / c;, entoncesxyztambién siempre serán iguales
- Aunque
-
Variables
consty variable length array- Aunque se usen variables calificadas con
constcomonymcomo tamaño de arreglo, estas no son una integer constant expression en C - En C11 § 6.6 ¶ 6, integer constant expression se limita a integer constant, enumeration constant, character constant,
sizeof,_Alignofy el operando inmediato de un cast cuyo resultado sea una integer constant, entre otros casos similares - Si la expresión de tamaño del arreglo no es una integer constant expression, entonces según C11 § 6.7.6.2 ¶ 4 se convierte en un variable length array
- Los variable length array no se permiten en file scope, por lo que la compilation unit que tiene el arreglo global
xno compilará - En block scope sí se permiten los variable length array, así que la compilation unit que tiene el arreglo local
ypuede compilar - Los variable length array son una conditional feature que una implementación no está obligada a soportar, por lo que en compiladores que no los soportan, incluso el ejemplo de block scope podría no compilar
- En C++, ambas compilation unit compilan, y como C++ no tiene el concepto de variable length array,
yse compila como un arreglo normal de 42 elementos
- Aunque se usen variables calificadas con
Declaración de funciones, valores de retorno y linkage
-
foo()yfoo(void)- Una declaración de función con la forma
foo()declara una función cuyo número y tipo de argumentos se desconocen, mientras quefoo(void)declara una función nular - Esta diferencia se trata en un artículo sobre declaraciones, definiciones y prototipos de funciones
- Como una declaración sin lista de argumentos solo introduce el nombre de la función y no define la cantidad ni los tipos de argumentos, puede ser legal en combinación con una definición posterior de la función
- Si se llama a una función sin prototipo, se aplican las promociones predeterminadas de argumentos y
floatse promueve adouble - Si el tipo de la función después de la promoción no es compatible con el tipo de la definición real de la función, la combinación de declaración y definición no es válida
- Una llamada a función sin declaración puede compilar en C porque se permitían funciones implícitas, pero en C++ es un error de compilación
- Si se hace una llamada como
bar(42)sin declaración, se aplican las promociones de argumentos enteros y42se representa comoint, por lo que sibarno es compatible conT (*)(int)para algún tipo de retornoT, el comportamiento es indefinido
- Una declaración de función con la forma
-
Funciones que devuelven valor pero no retornan un valor
- Aunque una función con tipo de retorno
intno devuelva un valor, en C puede ser legal siempre que no se use el valor del resultado de la llamada - En K&R C no existía el tipo
voidy si se omitía el tipo se asumía el tipo predeterminadoint, por lo que históricamente las funciones que no devuelven valor y la regla deintimplícito están relacionadas - La regla de
intimplícito fue eliminada en C99, y la discusión relacionada aparece en N661 y en el rationale de C99 - C17 § 6.9.1 ¶ 12 establece que si se alcanza la
}al final de la función y el llamador usa el valor de la llamada, eso es comportamiento indefinido - En C++98 § 6.6.3 ¶ 2, el simple hecho de llegar al final de una función que devuelve valor equivale a un
returnsin valor, y en una función que devuelve valor eso produce comportamiento indefinido - Como los compiladores de C++ generalmente no pueden demostrar en qué ramas
abort_program()termina la ejecución, en estos casos normalmente solo pueden emitir un diagnóstico en vez de un error
- Aunque una función con tipo de retorno
-
linkageyextern- Si en un alcance donde es visible una declaración previa se vuelve a declarar el mismo identificador con
extern, ellinkagede la declaración posterior es el mismo que el de la declaración previa - C17 § 6.2.2 ¶ 4 especifica que si la declaración previa indicó
linkageinterno o externo, la declaración posterior conexterntambién tiene el mismolinkage - Si la declaración previa no es visible o no tenía
linkage, el identificador conexterntienelinkageexterno - La combinación de declaraciones en el orden inverso puede producir comportamiento indefinido, y GCC y Clang lo detectan
- Si en un alcance donde es visible una declaración previa se vuelve a declarar el mismo identificador con
Calificadores y tipos incompletos
-
consten parámetros de función- Si en la declaración de una función el parámetro
xestá calificado conconstpero en la definición de la función no lo está, y en el cuerpo de la función se escribe un valor enx, eso es legal - Según C11 § 6.7.6.3 ¶ 15, al determinar la compatibilidad de tipos de parámetros de función y el tipo compuesto, cada parámetro declarado con un tipo calificado se trata como su versión no calificada
- El mismo tema también se trata en DR040
- Si en la declaración de una función el parámetro
-
consten el tipo de retorno de una función- Si solo el tipo de retorno de la definición de la función está calificado con
consty la declaración no lo está, la respuesta no puede considerarse simplemente correcta o incorrecta - El consenso general es que los calificadores de un rvalue deberían ignorarse, pero la redacción del estándar hasta C11 no lo trataba explícitamente
- En C17 quedó claro que los calificadores de rvalue deben ignorarse en cast, conversión de lvalue y declaradores de función
- C17 § 6.7.6.3 ¶ 5 especifica que el tipo que devuelve la función es la versión no calificada de
T, y esta redacción se agregó en C17 - La asignación de tipos de función puede ser legal incluso si difieren en el calificador
constdel tipo de retorno - La discusión adicional aparece en DR423 y DR481
- Si solo el tipo de retorno de la definición de la función está calificado con
-
structincompleta y variables globales- Aunque
struct foosea un tipo incompleto al momento de la declaración de una variable global y por eso no se conozca su tamaño, en ciertos casos se permite si el tipo se completa más adelante en la misma translation unit - Una lógica parecida también se aplica a variables globales o arreglos de tipo incompleto
- Esto también se trata en DR016
- Aunque
-
Objeto externo de tipo
void- Una declaración de variable de tipo
voidconlinkageinterno no es legal, pero una declaración de variable de tipovoidconlinkageexterno es legal desde el punto de vista sintáctico y no está prohibida explícitamente en ninguna parte del estándar C11 - Según C11 § 6.2.5 ¶ 19, el tipo
voides un tipo de objeto incompleto no completable compuesto por un conjunto vacío de valores - C11 § 6.3.2.1 ¶ 1 define un lvalue como una expresión de tipo objeto distinto de
void, por lo que el nombre del objetofoode tipovoidno es un lvalue válido - Bajo C11 es difícil imaginar operaciones significativas y conformes sobre un objeto externo
void - DR012 trata el caso en que el tipo se cambia a
const void, donde tomar la dirección del objetofoosí es legal, lo que parece más un descuido que una funcionalidad intencional
- Una declaración de variable de tipo
-
Conversión de puntero a
const- Cuando
Tes un tipo de objeto derivado, la asignación acpes legal, pero no hay una respuesta corta sobre si la asignación acpptambién lo es - Este tema se trata en un artículo sobre la conversión implícita de puntero a const
- Cuando
Arreglos, literales de cadena y ajuste de punteros
-
Los arreglos no son punteros
- Inicializar un arreglo e inicializar un puntero no es equivalente
- La primera forma inicializa un arreglo modificable con duración de almacenamiento automática o estática
- La segunda forma inicializa un puntero que apunta a un arreglo con duración de almacenamiento estática, y ese arreglo no necesariamente es modificable
- Un arreglo no es un puntero, y los detalles se tratan en este artículo relacionado
-
a,&a,&a[0]- En
int a[42];,a,&ay&a[0]se evalúan todos como la dirección del primer elemento del arreglo - Pero los tipos de las tres expresiones son distintos entre sí, por lo que no pueden usarse indistintamente
- Los detalles se tratan en este artículo relacionado
- En
-
Parámetros de arreglo y arreglos locales
- Si el tipo de un parámetro de función es “arreglo de
T”, se ajusta a “puntero aT” - Aunque el parámetro
xparezca serint[42], en realidad se trata comoint * - Si la variable local
yesint[42], entoncessizeof(y)es42 * sizeof(int) - Como en general el tamaño de un puntero a objeto no es igual al tamaño de 42 enteros,
sizeof(x) == sizeof(y)normalmente esfalse - Los detalles se tratan en este artículo relacionado
- Si el tipo de un parámetro de función es “arreglo de
Operadores, orden de evaluación y flujo de control
-
x+++y- En C no se pueden definir operadores nuevos como en C++, así que no existe ningún operador nuevo como
+++ x+++yse interpreta como una combinación de operadores existentes y es equivalente a(x++) + y--*--ptampoco es un operador nuevo, sino una combinación de operadores existentes--*--pes equivalente a--(*(--p))y, en el ejemplo, se evalúa como-1y como efecto secundario asigna-1ax[0]
- En C no se pueden definir operadores nuevos como en C++, así que no existe ningún operador nuevo como
-
Orden de evaluación de los operandos aritméticos
- La precedencia de operadores está bien definida, pero el orden de evaluación de los operandos aritméticos no está definido
(x=1) + (x=2)es comportamiento indefinido porque el orden de las dos asignaciones no está definido, así que no se determina si el valor final dexes1o2- Con la opción
-std=c11 -O2, GCC 8.2.1 evalúa la expresión de ejemplo como4, mientras que Clang 7.0.0 la evalúa como3
-
Orden de evaluación de los operadores lógicos
- En los operadores lógicos
&&y||, el orden de evaluación de los operandos también está bien definido - En la terminología del estándar de C, existe un sequence point entre la evaluación del primer operando y la del segundo
- En el ejemplo, primero se evalúa
x=1, que datrue, y luego se evalúax=2, que también datrue, por lo que la expresión completa datrue
- En los operadores lógicos
-
Estructura libre del cuerpo de
switch- El cuerpo de una sentencia
switchpuede ser cualquier statement, así que también puede ser legal una estructura mezclada con un loop y unif - Incluso si está dentro de la rama
truede una sentenciaifcuya expresión de control siempre esfalse, si existe una etiquetacase, esa sentencia pasa a estar activa yprintf("1");no es código muerto - Si se salta a
case 2, puede que no se ejecuten la clause-1 del loop ni la expresión de control, por lo que la variableidebe estar inicializada de antemano - Aunque ocurra fall through por no haber
breakencase 1, sicase 1está en la ramatruedelifycase 2en la ramafalse, se puede omitircase 2y continuar concase 3 - Después de las tres llamadas
foo(0); foo(1); foo(2);, la salida de la consola es02313223 - Un ejemplo real y famoso de mezclar loop y switch es Duff's device
- El cuerpo de una sentencia
Vida útil de objetos temporales y diferencias entre versiones del estándar C
- Un fragmento de código específico es comportamiento indefinido en C11, pero puede no serlo en C99
- En C11, la vida útil de cierto objeto se acorta, de modo que el objeto devuelto por una llamada a función solo vive mientras se evalúa el operando derecho
- En C99, ese mismo objeto vive hasta el final del bloque contenedor
- Referenciar un objeto cuya vida útil ya terminó es comportamiento indefinido según C11 § 6.2.4 ¶ 2
- Incluso en C99, la vida útil de un objeto con automatic storage duration está ligada al bloque contenedor más cercano, así que referenciar el objeto fuera de ese bloque es comportamiento indefinido
- C11 § 6.2.4 ¶ 8 establece que una expresión no-lvalue de tipo struct o union, si incluye un array member, hace referencia a un objeto con automatic storage duration y temporary lifetime
- La vida útil de este objeto temporal comienza cuando se evalúa la expresión y termina al finalizar la evaluación de la full expression o full declarator contenedor
- Intentar modificar un objeto con temporary lifetime es comportamiento indefinido
- El ejemplo correspondiente fue tomado de N1285, donde también hay discusión adicional
1 comentarios
Opiniones en Lobste.rs
La pregunta 4 no es válida en C23, aunque antes sí lo era
La pregunta 10 no es ni correcta ni incorrecta, así que molesta un poco para ser de opción múltiple
La pregunta 15 es técnicamente incorrecta, especialmente en relación con la pregunta 13, y la pregunta 20 es “no especificado”, así que tampoco corresponde a ninguna respuesta
La pregunta 30 es ambigua según cómo se lea
Aun así, acerté 27 de 31, y el hecho de ser desarrollador de compiladores ayudó un poco
Después de resolver unas cuatro preguntas, desapareció la sensación que todavía me quedaba de que C es lo bastante simple como para usarlo en un proyecto personal
clangcon-std=<language-standard>-pedantic -Wall -Wextray de verdad corriges cada warning que salga, además de evitar en lo posible los casts de punteros y la manipulación de punteros, parece que no hay tantas trampas grandesHoy en día los warnings de GCC/
clangson bastante buenos, y en <language-standard> puedes usar c89, c99, c11 o c23Si usas un compilador como tcc, que no hace optimizaciones raras, te toparás con menos sorpresas extrañas
Simplemente elegí según el criterio de “¿cuál sería el comportamiento más absurdo aquí?” y acerté 21 de 32
Casi todos mis errores fueron por no pensar lo bastante a fondo en el nivel de ese absurdo
Hace más de 15 años que apenas toqué C, y ver este tipo de quiz no me da ganas de retomarlo
Según C23, la respuesta de la pregunta 4 no es válida
Curiosamente, no he usado C en bastante tiempo y aun así acerté 27 de 32
Por cosas como esta he dependido de analizadores estáticos y linters
Ya desde la pregunta 1 me dio mala espina
No se tomó en cuenta de dónde podían venir esos punteros, y para que el caso que mencionan se cumpla hacen falta condiciones muy específicas
En la mayoría de los casos, el solo hecho de intentar crear el puntero ya es comportamiento indefinido, pero aun así supongo que puede considerarse justo
La pregunta 3 sí me sorprendió de verdad, otra trampa más de C
De entrada, que los literales enteros en C tengan un tipo fijado es bastante irritante
Las reglas de promoción de enteros lo compensan en cierta medida, pero también son una fuente de errores
Los lenguajes modernos deberían, en su mayoría o incluso todos, prohibir los casts numéricos implícitos y, cuando sea posible, inferir el tipo de los literales a partir del contexto; si no es posible, exigir un cast explícito
Después de la pregunta 6 dejé de confiar en el test y lo abandoné
Al principio fue porque la respuesta de la pregunta 5 parecía estar diseñada para hacer que prácticamente fallaras la 6, pero al revisarlo otra vez, da la impresión de que la propia pregunta 6 está mal
La explicación dice que la llamada a la función es comportamiento indefinido, pero la pregunta era si la definición de la función era legal, y probablemente sí lo era
Y no parece que eso sea un caso tan raro
El problema de
switch()estuvo muy buenoEra complicado, pero el proceso de resolverlo mentalmente fue muy divertido