tipos - ¿Debe usar siempre ''int'' para los números en C, incluso si no son negativos?
suma de n numeros en c++ con while (16)
Siempre uso int sin signo para valores que nunca deben ser negativos. Pero hoy me di cuenta de esta situación en mi código:
void CreateRequestHeader( unsigned bitsAvailable, unsigned mandatoryDataSize,
unsigned optionalDataSize )
{
If ( bitsAvailable – mandatoryDataSize >= optionalDataSize ) {
// Optional data fits, so add it to the header.
}
// BUG! The above includes the optional part even if
// mandatoryDataSize > bitsAvailable.
}
¿Debo comenzar a usar int en lugar de int sin firmar para los números, incluso si no pueden ser negativos?
¿Debería siempre ...
La respuesta a "Si yo siempre ..." es casi seguro que "no", hay muchos factores que determinan si debe usar un tipo de datos; la consistencia del tipo de datos es importante.
Pero, esta es una pregunta altamente subjetiva, es realmente fácil desordenar sin firmar:
for (unsigned int i = 10; i >= 0; i--);
resulta en un bucle infinito.
Esta es la razón por la que algunas guías de estilo, incluida la Guía de estilo C ++ de Google, desalientan los tipos de datos unsigned
.
En mi opinión personal, no me he encontrado con muchos errores causados por estos problemas con tipos de datos no firmados, yo diría que usa afirmaciones para verificar tu código y las usa con criterio (y menos cuando estás haciendo aritmética).
Algunos casos en los que debería usar tipos de enteros sin signo son:
- Debe tratar un dato como una representación binaria pura.
- Necesitas la semántica de la aritmética de módulo que obtienes con números sin firmar.
- Debe interactuar con el código que utiliza tipos sin signo (por ejemplo, rutinas de biblioteca estándar que aceptan / devuelven valores
size_t
.
Pero para la aritmética general, la cosa es que cuando dices que algo "no puede ser negativo", eso no significa necesariamente que debas usar un tipo sin signo. Debido a que puede poner un valor negativo en una sin firma, es solo que se convertirá en un valor realmente grande cuando lo saque. Por lo tanto, si quiere decir que los valores negativos están prohibidos, como en el caso de una función de raíz cuadrada básica, entonces está indicando una condición previa de la función y debe afirmar. Y no puedes afirmar que lo que no puede ser, es; necesita una forma de mantener los valores fuera de banda para poder probarlos (este es el mismo tipo de lógica detrás de getchar()
devuelve int
y no char
).
Además, la elección de firmado / no firmado también puede tener repercusiones prácticas en el rendimiento. Eche un vistazo al siguiente código:
#include <stdbool.h>
bool foo_i(int a) {
return (a + 69) > a;
}
bool foo_u(unsigned int a)
{
return (a + 69u) > a;
}
Ambos foo
''s son iguales excepto por el tipo de su parámetro. Pero, cuando se compila con c99 -fomit-frame-pointer -O2 -S
, se obtiene:
.file "try.c" .text .p2align 4,,15 .globl foo_i .type foo_i, @function foo_i: movl $1, %eax ret .size foo_i, .-foo_i .p2align 4,,15 .globl foo_u .type foo_u, @function foo_u: movl 4(%esp), %eax leal 69(%eax), %edx cmpl %eax, %edx seta %al ret .size foo_u, .-foo_u .ident "GCC: (Debian 4.4.4-7) 4.4.4" .section .note.GNU-stack,"",@progbits
Puedes ver que foo_i()
es más eficiente que foo_u()
. Esto se debe a que el desbordamiento aritmético no firmado está definido por el estándar como "envolvente", por lo que (a + 69u)
puede muy bien ser más pequeño que a
si a
es muy grande, y por lo tanto, debe haber un código para este caso. Por otro lado, el desbordamiento aritmético con signo no está definido, por lo que GCC seguirá adelante y asumirá que la aritmética con signo no se desborda, por lo que (a + 69)
nunca puede ser menor que a
. La elección indiscriminada de tipos sin firma puede, por lo tanto, afectar el rendimiento innecesariamente.
Bjarne Stroustrup, creador de C ++, advierte sobre el uso de tipos sin firma en su libro El lenguaje de programación C ++:
Los tipos de enteros sin signo son ideales para usos que tratan el almacenamiento como una matriz de bits. El uso de un unsigned en lugar de un int para obtener un bit más para representar enteros positivos es casi nunca una buena idea. Los intentos de asegurarse de que algunos valores sean positivos declarando las variables sin firmar normalmente serán vencidos por las reglas de conversión implícitas.
De los comentarios en una de las publicaciones del blog de Eric Lipperts (ver here ):
Jeffrey L. Whitledge
Una vez desarrollé un sistema en el que los valores negativos no tenían sentido como parámetro, así que en lugar de validar que los valores de los parámetros no eran negativos, pensé que sería una gran idea simplemente usar uint en su lugar. Descubrí rápidamente que cada vez que usaba esos valores para cualquier cosa (como llamar a los métodos BCL), se habían convertido en enteros con signo. Esto significaba que tenía que validar que los valores no excedían el rango entero con signo en el extremo superior, por lo que no gané nada. Además, cada vez que se llamaba al código, los ints que se estaban utilizando (a menudo recibidos de las funciones BCL) tenían que convertirse en uints. No pasó mucho tiempo antes de que cambié todos esos puntos de vista de nuevo a ints y tomé todo ese reparto innecesario. Todavía tengo que validar que los números no son negativos, ¡pero el código es mucho más limpio!
Eric Lippert
No podría haberlo dicho mejor. Casi nunca necesita el rango de una uint, y no son compatibles con CLS. La forma estándar de representar un entero pequeño es con "int", incluso si hay valores allí que están fuera de rango. Una buena regla general: solo use "uint" para las situaciones en las que está interactuando con un código no administrado que espera uints, o donde el número entero en cuestión se usa claramente como un conjunto de bits, no como un número. Siempre trata de evitarlo en las interfaces públicas. - Eric
La respuesta es sí. El tipo int "unsigned" de C y C ++ no es un "entero siempre positivo", sin importar cómo se vea el nombre del tipo. El comportamiento de los ints sin firma C / C ++ no tiene sentido si intenta leer el tipo como "no negativo" ... por ejemplo:
- La diferencia de dos sin signo es un número sin signo (no tiene sentido si lo lees como "La diferencia entre dos números no negativos no es negativa")
- La adición de un int y un unsigned int no está firmado
- Hay una conversión implícita de int a unsigned int (si lees unsigned como "non-negative" es la conversión opuesta lo que tendría sentido)
- Si declara que una función acepta un parámetro sin signo cuando alguien pasa un int negativo, simplemente lo convierte implícitamente en un valor positivo enorme; en otras palabras, utilizar un tipo de parámetro sin signo no ayuda a encontrar errores ni en tiempo de compilación ni en tiempo de ejecución.
De hecho, los números sin firmar son muy útiles para ciertos casos porque son elementos del anillo "integers-modulo-N", siendo N una potencia de dos. Los ints sin firmar son útiles cuando desea utilizar esa aritmética de módulo n, o como máscaras de bits; NO son útiles como cantidades.
Desafortunadamente, en C y C ++ sin signo también se usaron para representar cantidades no negativas para poder usar los 16 bits cuando los números enteros donde ese pequeño ... en ese momento poder usar 32k o 64k se consideraron una gran diferencia. Lo clasificaría básicamente como un accidente histórico ... no deberías intentar leer una lógica porque no había lógica.
Por cierto, en mi opinión, eso fue un error ... si 32k no son suficientes, entonces muy pronto, 64k tampoco será suficiente; abusar del módulo entero solo debido a un bit extra en mi opinión fue un costo demasiado alto para pagar. Por supuesto, habría sido razonable hacerlo si estuviera presente o definido un tipo correcto no negativo ... pero la semántica sin signo es errónea si se usa como no negativa.
A veces, puede encontrar quién dice que unsigned es bueno porque "documenta" que solo desea valores no negativos ... sin embargo, esa documentación es de cualquier valor solo para personas que realmente no saben cómo funciona unsigned para C o C ++. Para mí, ver un tipo sin signo utilizado para valores no negativos simplemente significa que la persona que escribió el código no entendió el idioma en esa parte.
Si realmente entiendes y quieres el comportamiento de "envoltura" de las entradas sin firmar, entonces son la elección correcta (por ejemplo, casi siempre uso "caracteres sin firmar" cuando manejo bytes); Si no va a utilizar el comportamiento de ajuste (y ese comportamiento solo va a ser un problema para usted en el caso de la diferencia que mostró), este es un claro indicador de que el tipo sin firma es una mala elección. debe pegarse con los ints llanos.
¿Esto significa que el tipo de retorno de C ++ std::vector<>::size()
es una mala elección? Sí ... es un error. Pero si usted lo dice, prepárese para que lo llamen malos nombres por quien no entiende que el nombre "sin firmar" es solo un nombre ... lo que cuenta es el comportamiento y ese es un comportamiento "módulo-n" (y no uno consideraría que un tipo "módulo-n" para el tamaño de un contenedor es una opción sensata).
La situación en la que (bitsAvailable – mandatoryDataSize)
produce un resultado ''inesperado'' cuando los tipos no están firmados y bitsAvailable < mandatoryDataSize
es una razón por la que a veces se usan tipos con signo incluso cuando se espera que los datos nunca sean negativos.
Creo que no hay reglas estrictas y rápidas. Por lo general, "predeterminado" es el uso de tipos no firmados para los datos que no tienen ninguna razón para ser negativos, pero luego hay que tomarlos para asegurarse de que el ajuste aritmético no exponga los errores.
Por otra parte, si utiliza tipos firmados, a veces aún debe considerar el desbordamiento:
MAX_INT + 1
La clave es que debes tener cuidado al realizar operaciones aritméticas para este tipo de errores.
No puede evitar por completo los tipos sin firma en el código portátil, porque muchas de las definiciones de tipo en la biblioteca estándar no están firmadas (principalmente en size_t
), y muchas funciones las devuelven (por ejemplo, std::vector<>::size()
).
Dicho esto, generalmente prefiero ceñirme a los tipos firmados siempre que sea posible por las razones que ha descrito. No es solo el caso que mencionas, en el caso de una aritmética mixta firmada / no firmada, el argumento firmado se promociona silenciosamente a unsigned.
No sé si es posible en c, pero en este caso, simplemente le diría lo de XY a un int.
No, debe usar el tipo correcto para su aplicación. No hay regla de oro. A veces, en microcontroladores pequeños, por ejemplo, es más rápido y eficiente en memoria utilizar, digamos variables de 8 o 16 bits siempre que sea posible, ya que a menudo es el tamaño de la ruta de datos nativa, pero ese es un caso muy especial. También recomiendo usar stdint.h siempre que sea posible. Si está utilizando Visual Studio, puede encontrar versiones con licencia de BSD.
Parece que estoy en desacuerdo con la mayoría de las personas aquí, pero los tipos unsigned
me parecen muy útiles, pero no en su forma histórica original.
Si, por lo tanto, se adhiere a la semántica que un tipo representa para usted, entonces no debería haber ningún problema: use size_t
(sin signo) para los índices de matriz, las compensaciones de datos, etc. off_t
(firmado) para las compensaciones de archivos. Use ptrdiff_t
(firmado) para las diferencias de punteros. Use uint8_t
para los enteros sin signo pequeños y int8_t
para los que tienen int8_t
. Y evita al menos el 80% de los problemas de portabilidad.
Y no use int
, long
, unsigned
, char
si no debe. Pertenecen a los libros de historia. (A veces debe hacerlo, devoluciones de error, campos de bits, por ejemplo)
Y para volver a su ejemplo:
bitsAvailable – mandatoryDataSize >= optionalDataSize
puede ser fácilmente reescrito como
bitsAvailable >= optionalDataSize + mandatoryDataSize
lo que no evita el problema de un posible desbordamiento ( assert
es tu amigo) pero te acerca un poco más a la idea de lo que quieres probar, creo.
Si existe la posibilidad de desbordamiento, entonces asigne los valores al siguiente tipo de datos más alto durante el cálculo, es decir:
void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize )
{
signed __int64 available = bitsAvailable;
signed __int64 mandatory = mandatoryDataSize;
signed __int64 optional = optionalDataSize;
if ( (mandatory + optional) <= available ) {
// Optional data fits, so add it to the header.
}
}
De lo contrario, simplemente compruebe los valores individualmente en lugar de calcular:
void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize )
{
if ( bitsAvailable < mandatoryDataSize ) {
return;
}
bitsAvailable -= mandatoryDataSize;
if ( bitsAvailable < optionalDataSize ) {
return;
}
bitsAvailable -= optionalDataSize;
// Optional data fits, so add it to the header.
}
Si sus números nunca deben ser menores que cero, pero tienen la posibilidad de ser <0, utilice enteros con signo y aspire aserciones u otras comprobaciones de tiempo de ejecución. Si realmente está trabajando con valores de 32 bits (o 64, o 16, dependiendo de su arquitectura de destino) donde el bit más significativo significa algo distinto de "-", solo debe usar variables sin signo para mantenerlos. Es más fácil detectar los desbordamientos de enteros donde un número que siempre debería ser positivo es muy negativo que cuando es cero, por lo que si no necesita ese bit, vaya con los firmados.
Supongamos que necesita contar de 1 a 50000. Puede hacerlo con un entero sin signo de dos bytes, pero no con un entero con signo de dos bytes (si el espacio es tan importante).
Tendrá que mirar los resultados de las operaciones que realiza en las variables para verificar si puede superar los flujos excesivos / insuficientes, en su caso, el resultado es potencialmente negativo. En ese caso, es mejor utilizar los equivalentes firmados.
Una cosa que no se ha mencionado es que el intercambio de números firmados / sin firmar puede dar lugar a errores de seguridad . Este es un gran problema, ya que muchas de las funciones en la biblioteca C estándar toman / devuelven números sin firmar (fread, memcpy, malloc, etc., todos toman parámetros size_t
)
Por ejemplo, tome el siguiente ejemplo inocuo (del código real):
//Copy a user-defined structure into a buffer and process it
char* processNext(char* data, short length)
{
char buffer[512];
if (length <= 512) {
memcpy(buffer, data, length);
process(buffer);
return data + length;
} else {
return -1;
}
}
Parece inofensivo, ¿verdad? El problema es que la length
está firmada, pero se convierte en no firmada cuando se pasa a memcpy
. Por lo tanto, establecer la longitud en SHRT_MIN
validará la prueba <= 512
, pero causará que memcpy
copie más de 512 bytes al búfer; esto permite que un atacante sobrescriba la dirección de retorno de la función en la pila y (después de un poco de trabajo) se haga cargo de su ¡computadora!
Puede estar ingenuamente diciendo: "Es tan obvio que la longitud debe ser size_t
o debe ser >= 0
, nunca podría cometer ese error" . Excepto, te garantizo que si alguna vez has escrito algo no trivial, lo has hecho. Así tienen los autores de Windows , Linux , BSD , Solaris , Firefox , OpenSSL , Safari , MS Paint , Internet Explorer , Google Picasa , Opera , Flash , Oficina abierta , Subversion , Apache , Python , PHP , Pidgin , Gimp , ... una y otra y otra vez ... - y estas son personas brillantes cuyo trabajo es conocer la seguridad.
En resumen, siempre use size_t
para tamaños.
Hombre, la programación es difícil .
if (bitsAvailable >= optionalDataSize + mandatoryDataSize) {
// Optional data fits, so add it to the header.
}
Libre de errores, siempre que obligatorioDataSize + optionalDataSize no pueda desbordar el tipo entero sin signo; la denominación de estas variables me lleva a creer que este es probablemente el caso.