tips - Funciones ocultas de C
tips moto c (30)
Sé que hay un estándar detrás de todas las implementaciones del compilador de C, por lo que no debe haber funciones ocultas. A pesar de eso, estoy seguro de que todos los desarrolladores C tienen trucos ocultos / secretos que usan todo el tiempo.
Al inicializar matrices o enumeraciones, puede poner una coma después del último elemento en la lista de inicializadores. p.ej:
int x[] = { 1, 2, 3, };
enum foo { bar, baz, boom, };
Esto se hizo para que, si está generando código automáticamente, no tenga que preocuparse por eliminar la última coma.
Aserciones en tiempo de compilación, como ya se discutió aquí .
//--- size of static_assertion array is negative if condition is not met
#define STATIC_ASSERT(condition) /
typedef struct { /
char static_assertion[condition ? 1 : -1]; /
} static_assertion_t
//--- ensure structure fits in
STATIC_ASSERT(sizeof(mystruct_t) <= 4096);
Bueno ... Creo que uno de los puntos fuertes del lenguaje C es su portabilidad y estandarización, así que cada vez que encuentro algún "truco oculto" en la implementación que estoy usando, trato de no usarlo porque trato de mantener mi El código C es lo más estándar y portátil posible.
Bueno, nunca lo he usado, y no estoy seguro de si alguna vez lo recomendaría a alguien, pero creo que esta pregunta sería incompleta sin mencionar el truco de rutina de Simon Tatham .
C tiene un compilador C estándar pero no todos son totalmente compatibles (¡aún no he visto ningún compilador C99 totalmente compatible!).
Dicho esto, los trucos que prefiero son aquellos que no son obvios y son portátiles en todas las plataformas ya que dependen de la semántica C. Por lo general, se trata de macros o bit aritmética.
Por ejemplo: intercambiando dos enteros sin signo sin usar una variable temporal:
...
a ^= b ; b ^= a; a ^=b;
...
o "extendiendo C" para representar máquinas de estado finito como:
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
eso se puede lograr con las siguientes macros:
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
En general, sin embargo, no me gustan los trucos que son inteligentes pero hacen que el código sea innecesariamente complicado de leer (como el ejemplo de intercambio) y me encantan los que hacen que el código sea más claro y transmita directamente la intención (como el ejemplo de FSM) .
C99 tiene una impresionante inicialización de estructura de orden.
struct foo{
int x;
int y;
char* name;
};
void main(){
struct foo f = { .y = 23, .name = "awesome", .x = -38 };
}
Comprobación de suposiciones en tiempo de compilación usando enums: Ejemplo estúpido, pero puede ser realmente útil para bibliotecas con constantes configurables en tiempo de compilación.
#define D 1
#define DD 2
enum CompileTimeCheck
{
MAKE_SURE_DD_IS_TWICE_D = 1/(2*(D) == (DD)),
MAKE_SURE_DD_IS_POW2 = 1/((((DD) - 1) & (DD)) == 0)
};
Constantes de múltiples caracteres:
int x = ''ABCD'';
Esto establece x
a 0x41424344
(o 0x44434241
, dependiendo de la arquitectura).
EDITAR: esta técnica no es portátil, especialmente si serializas el int. Sin embargo, puede ser extremadamente útil crear enumeraciones autodocumentadas. p.ej
enum state {
stopped = ''STOP'',
running = ''RUN!'',
waiting = ''WAIT'',
};
Esto lo hace mucho más sencillo si está mirando un volcado de memoria en bruto y necesita determinar el valor de una enumeración sin tener que buscarla.
Cuando use sscanf, puede usar% n para averiguar dónde debe continuar leyendo:
sscanf ( string, "%d%n", &number, &length );
string += length;
Aparentemente, no puedes agregar otra respuesta, así que incluiré una segunda aquí, puedes usar "&&" y "||" como condicionales:
#include <stdio.h>
#include <stdlib.h>
int main()
{
1 || puts("Hello/n");
0 || puts("Hi/n");
1 && puts("ROFL/n");
0 && puts("LOL/n");
exit( 0 );
}
Este código dará como resultado:
Hi ROFL
Descubrí recientemente 0 campos de bit.
struct {
int a:3;
int b:2;
int :0;
int c:4;
int d:3;
};
que dará un diseño de
000aaabb 0ccccddd
en lugar de sin el: 0;
0000aaab bccccddd
El campo de ancho 0 indica que los siguientes campos de bits se deben establecer en la siguiente entidad atómica ( char
)
El operador de coma no se usa ampliamente. Ciertamente puede ser abusado, pero también puede ser muy útil. Este uso es el más común:
for (int i=0; i<10; i++, doSomethingElse())
{
/* whatever */
}
Pero puedes usar este operador en cualquier lugar. Observar:
int j = (printf("Assigning variable j/n"), getValueFromSomewhere());
Cada declaración se evalúa, pero el valor de la expresión será el de la última declaración evaluada.
Entrelazamiento de estructuras como el dispositivo de Duff :
strncpy(to, from, count)
char *to, *from;
int count;
{
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
Extraña indexación vectorial:
int v[100]; int index = 10;
/* v[index] it''s the same thing as index[v] */
Gcc (c) tiene algunas características divertidas que puede habilitar, como las declaraciones de funciones anidadas, y la forma a?: B del operador?: Que devuelve a si a no es falso.
La asignación de la estructura es genial. Muchas personas no parecen darse cuenta de que las estructuras también son valores, y se pueden asignar, no es necesario usar memcpy()
, cuando una simple tarea funciona.
Por ejemplo, considere una biblioteca imaginaria de gráficos en 2D, podría definir un tipo para representar una coordenada de pantalla (entera):
typedef struct {
int x;
int y;
} Point;
Ahora, haces cosas que pueden parecer "incorrectas", como escribir una función que crea un punto inicializado a partir de los argumentos de la función, y lo devuelve, así:
Point point_new(int x, int y)
{
Point p;
p.x = x;
p.y = y;
return p;
}
Esto es seguro, siempre y cuando el valor de retorno se copie por valor usando la asignación de estructura:
Point origin;
origin = point_new(0, 0);
De esta forma, puede escribir un código bastante limpio y orientado a objetos, todo en un estándar simple C.
Las variables automáticas de tamaño variable también son útiles en algunos casos. Estos fueron agregados en iC99 y han sido soportados en gcc por un largo tiempo.
void foo(uint32_t extraPadding) {
uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
Usted termina con un buffer en la pila con espacio para el encabezado de protocolo de tamaño fijo más datos de tamaño variable. Puede obtener el mismo efecto con alloca (), pero esta sintaxis es más compacta.
Debes asegurarte de que extraPadding es un valor razonable antes de llamar a esta rutina, o terminas volando la pila. Debería verificar los argumentos antes de llamar malloc o cualquier otra técnica de asignación de memoria, así que esto no es realmente inusual.
Los compiladores C implementan uno de varios estándares. Sin embargo, tener un estándar no significa que se definan todos los aspectos del lenguaje. El dispositivo de Duff , por ejemplo, es una característica favorita "oculta" que se ha vuelto tan popular que los compiladores modernos tienen un código de reconocimiento de propósito especial para garantizar que las técnicas de optimización no afecten el efecto deseado de este patrón de uso frecuente.
En general, se desaconsejan las funciones ocultas o los trucos del idioma, ya que se ejecuta en el filo de cualquier estándar C que utilice su compilador. Muchos de estos trucos no funcionan de un compilador a otro, y muchas veces este tipo de características fallará de una versión de un compilador por un fabricante determinado a otra versión.
Varios trucos que han roto el código C incluyen:
- Confiando en cómo el compilador establece las estructuras en la memoria.
- Supuestos sobre la endianidad de enteros / flotantes.
- Supuestos sobre la función ABI.
- Suposiciones sobre la dirección en que crecen los armazones de pila.
- Suposiciones sobre el orden de ejecución dentro de las declaraciones.
- Suposiciones sobre el orden de ejecución de las declaraciones en los argumentos de la función.
- Suposiciones sobre el tamaño del bit o la precisión de los tipos short, int, long, float y double.
Otros problemas y problemas surgen cuando los programadores hacen suposiciones sobre los modelos de ejecución que están todos especificados en la mayoría de los estándares C como comportamientos ''dependientes del compilador''.
Más de un truco del compilador de GCC, pero puede dar pistas de indicación de rama al compilador (común en el kernel de Linux)
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
ver: http://kerneltrap.org/node/4705
Lo que me gusta de esto es que también agrega algo de expresividad a algunas funciones.
void foo(int arg)
{
if (unlikely(arg == 0)) {
do_this();
return;
}
do_that();
...
}
Macros de argumento variable de estilo C99, aka
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "/n", /
__VAR_ARGS__)
que se usaría como
ERR(errCantOpen, "File %s cannot be opened", filename);
Aquí también uso el operador de stringize y la concatenación constante de cadenas, otras características que realmente me gustan.
Mi función "oculta" favorita de C es el uso de% n en printf para volver a escribir en la pila. Normalmente, printf muestra los valores de los parámetros de la pila en función de la cadena de formato, pero% n puede volver a escribirlos.
Mira la sección 3.4.2 here . Puede conducir a muchas vulnerabilidades desagradables.
Nunca usé campos de bits, pero suenan bien para cosas de nivel ultra bajo.
struct cat {
unsigned int legs:3; // 3 bits for legs (0-4 fit in 3 bits)
unsigned int lives:4; // 4 bits for lives (0-9 fit in 4 bits)
// ...
};
cat make_cat()
{
cat kitty;
kitty.legs = 4;
kitty.lives = 9;
return kitty;
}
Esto significa que sizeof(cat)
puede ser tan pequeño como sizeof(char)
.
Comentarios incorporados de Aaron y leppie , gracias chicos.
Punteros de función. Puede utilizar una tabla de indicadores de función para implementar, por ejemplo, intérpretes rápidos de código de subprocesos indirectos (FORTH) o despachadores de códigos de bytes, o para simular métodos virtuales similares a OO.
Luego hay gemas ocultas en la biblioteca estándar, como qsort (), bsearch (), strpbrk (), strcspn () [las dos últimas son útiles para implementar un reemplazo de strtok ()].
Una mala característica de C es que el desbordamiento aritmético firmado es un comportamiento indefinido (UB). Por lo tanto, cada vez que vea una expresión como x + y, ambos firmados, podría desbordarse y causar UB.
Soy muy aficionado a los inicializadores designados, agregados en C99 (y soportados en gcc por un largo tiempo):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
La inicialización de la matriz ya no depende de la posición. Si cambia los valores de FOO o BAR, la inicialización de la matriz se corresponderá automáticamente con su nuevo valor.
Usar INT (3) para establecer el punto de ruptura en el código es mi favorito de todos los tiempos
estructuras anónimas y matrices es mi favorito. (Véase http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html )
setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));
o
void myFunction(type* values) {
while(*values) x=*values++;
}
myFunction((type[]){val1,val2,val3,val4,0});
incluso se puede usar para instanciar listas enlazadas ...
gcc tiene una serie de extensiones para el lenguaje C que disfruto, que se pueden encontrar here . Algunos de mis favoritos son atributos de funciones . Un ejemplo extremadamente útil es el atributo de formato. Esto se puede usar si define una función personalizada que toma una cadena de formato printf. Si habilita este atributo de función, gcc verificará sus argumentos para asegurarse de que su cadena de formato y sus argumentos coincidan y generará advertencias o errores según corresponda.
int my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));
la característica (oculta) que me "sorprendió" cuando vi por primera vez es sobre printf. esta característica le permite usar variables para formatear los especificadores de formato. busca el código, verás mejor:
#include <stdio.h>
int main() {
int a = 3;
float b = 6.412355;
printf("%.*f/n",a,b);
return 0;
}
el personaje * logra este efecto.
Concatenación constante de cadenas
Me sorprendió bastante no haberlo visto ya en las respuestas, ya que todos los compiladores que conozco lo admiten, pero muchos programadores parecen ignorarlo. A veces es realmente útil y no solo al escribir macros.
Caso de uso que tengo en mi código actual: Tengo un #define PATH "/some/path/"
en un archivo de configuración (realmente está configurado por el archivo MAKE). Ahora quiero compilar la ruta completa, incluidos los nombres de archivo para abrir los recursos. Simplemente va a:
fd = open(PATH "/file", flags);
En lugar de lo horrible, pero muy común:
char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
Tenga en cuenta que la solución horrible común es:
- tres veces más largo
- mucho menos fácil de leer
- mucho más lento
- menos potente en establecer un límite de tamaño de búfer arbitrario (pero tendría que usar código aún más largo para evitar eso sin contaminar las cadenas constantes).
- usa más espacio de pila
inicializando la estructura a cero
struct mystruct a = {0};
esto pondrá a cero todos los elementos de estructura.
int8_t
int16_t
int32_t
uint8_t
uint16_t
uint32_t
Estos son un elemento opcional en el estándar, pero debe ser una característica oculta, porque las personas los están redefiniendo constantemente. Una base de código en la que he trabajado (y todavía lo hago, por ahora) tiene varias redefiniciones, todas con diferentes identificadores. La mayoría de las veces es con macros de preprocesador:
#define INT16 short
#define INT32 long
Y así. Me da ganas de sacarme el pelo. Solo usa los enteros de tipo entero voluminosos.