So che esiste uno standard dietro tutte le implementazioni del compilatore C, quindi non dovrebbero esserci funzionalità nascoste. Nonostante ciò, sono sicuro che tutti gli sviluppatori C hanno trucchi nascosti / segreti che usano sempre.
So che esiste uno standard dietro tutte le implementazioni del compilatore C, quindi non dovrebbero esserci funzionalità nascoste. Nonostante ciò, sono sicuro che tutti gli sviluppatori C hanno trucchi nascosti / segreti che usano sempre.
Risposte:
Puntatori a funzione. È possibile utilizzare una tabella di puntatori a funzione per implementare, ad esempio, interpreti di codice a thread indiretto veloce (FORTH) o dispatcher di codice byte, o per simulare metodi virtuali simili a OO.
Quindi ci sono gemme nascoste nella libreria standard, come qsort (), bsearch (), strpbrk (), strcspn () [gli ultimi due sono utili per implementare una sostituzione strtok ()].
Un malfunzionamento di C è che l'overflow aritmetico firmato è un comportamento indefinito (UB). Quindi, ogni volta che vedi un'espressione come x + y, entrambi essendo ints firmati, potrebbe potenzialmente traboccare e causare UB.
Più di un trucco del compilatore GCC, ma puoi dare suggerimenti di indicazione di ramo al compilatore (comune nel kernel di Linux)
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
vedi: http://kerneltrap.org/node/4705
Quello che mi piace di questo è che aggiunge anche un po 'di espressività ad alcune funzioni.
void foo(int arg)
{
if (unlikely(arg == 0)) {
do_this();
return;
}
do_that();
...
}
int8_t
int16_t
int32_t
uint8_t
uint16_t
uint32_t
Si tratta di un elemento facoltativo nello standard, ma deve essere una funzione nascosta, poiché le persone li ridefiniscono costantemente. Una base di codice su cui ho lavorato (e lo faccio ancora, per ora) ha più ridefinizioni, tutte con identificatori diversi. Il più delle volte è con le macro del preprocessore:
#define INT16 short
#define INT32 long
E così via. Mi fa venire voglia di strapparmi i capelli. Basta usare i caratteri tipografici interi standard eccentrici!
L'operatore virgola non è ampiamente utilizzato. Può certamente essere maltrattato, ma può anche essere molto utile. Questo uso è il più comune:
for (int i=0; i<10; i++, doSomethingElse())
{
/* whatever */
}
Ma puoi usare questo operatore ovunque. Osservare:
int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
Ogni istruzione viene valutata, ma il valore dell'espressione sarà quello dell'ultima istruzione valutata.
inizializzazione della struttura a zero
struct mystruct a = {0};
questo azzererà tutti gli elementi della struttura.
memset
/ calloc
do "all bytes zero" (cioè zero fisici), che in effetti non è definito per tutti i tipi. { 0 }
è garantito per intilaizzare tutto con valori logici zero adeguati . I puntatori, ad esempio, sono garantiti per ottenere i loro valori null corretti, anche se lo è il valore null sulla piattaforma data 0xBAADFOOD
.
memset
fa ( 0
come secondo argomento). Si ottiene lo zero logico quando si inizializza / assegna 0
(o { 0 }
) all'oggetto nel codice sorgente. Questi due tipi di zeri non producono necessariamente lo stesso risultato. Come nell'esempio con puntatore. Quando lo fai memset
su un puntatore, ottieni un 0x0000
puntatore. Ma quando si assegna 0
a un puntatore, si ottiene un valore di puntatore nullo , che a livello fisico potrebbe essere 0xBAADF00D
o qualsiasi altra cosa.
double
,. Di solito è implementato secondo lo standard IEEE-754, in cui lo zero logico e lo zero fisico sono gli stessi. Ma IEEE-754 non è richiesto dalla lingua. Quindi potrebbe accadere che quando lo fai double d = 0;
(zero logico), alcuni bit nella memoria occupati d
non saranno zero.
Costanti multi-carattere:
int x = 'ABCD';
Imposta x
su 0x41424344
(o 0x44434241
, a seconda dell'architettura).
EDIT: questa tecnica non è portatile, soprattutto se si serializza int. Tuttavia, può essere estremamente utile creare enum documentali. per esempio
enum state {
stopped = 'STOP',
running = 'RUN!',
waiting = 'WAIT',
};
Questo rende molto più semplice se stai guardando un dump di memoria grezza e devi determinare il valore di un enum senza doverlo cercare.
Non ho mai usato i bit field ma suonano alla grande per cose di livello ultra basso.
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;
}
Ciò significa che sizeof(cat)
può essere piccolo come sizeof(char)
.
C ha uno standard ma non tutti i compilatori C sono completamente conformi (non ho ancora visto alcun compilatore C99 pienamente conforme!).
Detto questo, i trucchi che preferisco sono quelli non ovvi e portatili su tutte le piattaforme poiché si basano sul semantico C. Di solito si tratta di macro o bit aritmetica.
Ad esempio: scambiando due numeri interi senza segno senza utilizzare una variabile temporanea:
...
a ^= b ; b ^= a; a ^=b;
...
o "estendere C" per rappresentare macchine a stati finiti come:
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
che può essere ottenuto con le seguenti macro:
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
In generale, tuttavia, non mi piacciono i trucchi che sono intelligenti ma rendono il codice inutilmente complicato da leggere (come esempio di scambio) e adoro quelli che rendono il codice più chiaro e trasmettono direttamente l'intenzione (come nell'esempio di FSM) .
Strutture intrecciate come il dispositivo di 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);
}
}
Mi piacciono molto gli inizializzatori designati, aggiunti in C99 (e supportati in gcc da molto tempo):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
L'inizializzazione dell'array non dipende più dalla posizione. Se si modificano i valori di FOO o BAR, l'inizializzazione dell'array corrisponderà automaticamente al loro nuovo valore.
strutture e array anonimi è il mio preferito. (cfr. 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});
può anche essere usato per istanziare liste collegate ...
gcc ha una serie di estensioni al linguaggio C che mi piacciono, che puoi trovare qui . Alcuni dei miei preferiti sono attributi di funzione . Un esempio estremamente utile è l'attributo format. Questo può essere usato se si definisce una funzione personalizzata che accetta una stringa di formato printf. Se abiliti questo attributo di funzione, gcc eseguirà dei controlli sugli argomenti per assicurarti che la stringa di formato e gli argomenti coincidano e genererà avvisi o errori nel modo appropriato.
int my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));
la funzione (nascosta) che mi ha "scioccato" quando ho visto per la prima volta riguarda printf. questa funzione consente di utilizzare le variabili per la formattazione degli identificatori di formato stessi. cerca il codice, vedrai meglio:
#include <stdio.h>
int main() {
int a = 3;
float b = 6.412355;
printf("%.*f\n",a,b);
return 0;
}
il carattere * ottiene questo effetto.
Beh ... penso che uno dei punti di forza del linguaggio C sia la sua portabilità e standardità, quindi ogni volta che trovo qualche "trucco nascosto" nell'implementazione che sto attualmente usando, provo a non usarlo perché provo a mantenere il mio Codice C il più standard e portatile possibile.
Affermazioni in fase di compilazione, come già discusso qui .
//--- 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);
Concatenazione di stringhe costante
Sono stato piuttosto sorpreso di non averlo già visto nelle risposte, poiché tutti i compilatori che conosco lo supportano, ma molti programmatori sembrano ignorarlo. A volte è davvero utile e non solo quando si scrivono macro.
Caso d'uso che ho nel mio codice attuale: ho un #define PATH "/some/path/"
in un file di configurazione (in realtà è impostato dal makefile). Ora voglio costruire il percorso completo inclusi i nomi dei file per aprire le risorse. Va solo a:
fd = open(PATH "/file", flags);
Invece dell'orribile, ma molto comune:
char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
Si noti che la soluzione orribile comune è:
Bene, non l'ho mai usato, e non sono sicuro se lo consiglierei mai a nessuno, ma penso che questa domanda sarebbe incompleta senza menzionare il trucco di routine di Simon Tatham .
Quando si inizializzano matrici o enumerazioni, è possibile inserire una virgola dopo l'ultimo elemento nell'elenco di inizializzatori. per esempio:
int x[] = { 1, 2, 3, };
enum foo { bar, baz, boom, };
Questo è stato fatto in modo che se stai generando codice automaticamente non devi preoccuparti di eliminare l'ultima virgola.
L'assegnazione di Struct è interessante. Molte persone non sembrano rendersi conto che anche le strutture sono valori e possono essere assegnate in giro, non è necessario utilizzarlememcpy()
, quando un semplice compito fa la differenza.
Ad esempio, considera una libreria grafica 2D immaginaria, potrebbe definire un tipo per rappresentare una coordinata dello schermo (intera):
typedef struct {
int x;
int y;
} Point;
Ora fai cose che potrebbero sembrare "sbagliate", come scrivere una funzione che crea un punto inizializzato dagli argomenti della funzione e lo restituisce, in questo modo:
Point point_new(int x, int y)
{
Point p;
p.x = x;
p.y = y;
return p;
}
Questo è sicuro, fintanto che (ovviamente) il valore restituito viene copiato in base al valore usando l'assegnazione struct:
Point origin;
origin = point_new(0, 0);
In questo modo puoi scrivere codice ish abbastanza pulito e orientato agli oggetti, tutto in semplice standard C.
Indicizzazione vettoriale strana:
int v[100]; int index = 10;
/* v[index] it's the same thing as index[v] */
I compilatori C implementano uno dei numerosi standard. Tuttavia, avere uno standard non significa che tutti gli aspetti della lingua siano definiti. Il dispositivo di Duff , ad esempio, è una delle funzionalità "nascoste" preferite che è diventata così popolare che i compilatori moderni hanno un codice di riconoscimento per scopi speciali per garantire che le tecniche di ottimizzazione non ostacolino l'effetto desiderato di questo modello spesso usato.
In generale, le funzioni nascoste o i trucchi del linguaggio sono sconsigliati mentre si esegue sul filo del rasoio di qualsiasi standard C utilizzato dal compilatore. Molti di questi trucchi non funzionano da un compilatore all'altro e spesso questo tipo di funzionalità fallisce da una versione di una suite di compilatori da un determinato produttore a un'altra versione.
Vari trucchi che hanno rotto il codice C includono:
Altri problemi e problemi che sorgono quando i programmatori fanno ipotesi sui modelli di esecuzione che sono tutti specificati nella maggior parte degli standard C come comportamento "dipendente dal compilatore".
Quando usi sscanf puoi usare% n per scoprire dove dovresti continuare a leggere:
sscanf ( string, "%d%n", &number, &length );
string += length;
Apparentemente, non puoi aggiungere un'altra risposta, quindi includerò una seconda qui, puoi usare "&&" e "||" come condizionali:
#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 );
}
Questo codice genererà:
Ciao ROFL
usare INT (3) per impostare il punto di interruzione nel codice è il mio preferito di tutti i tempi
La mia funzione "nascosta" preferita di C, è l'uso di% n in printf per riscrivere nello stack. Normalmente printf estrae i valori dei parametri dallo stack in base alla stringa di formato, ma% n può riscriverli.
Dai un'occhiata alla sezione 3.4.2 qui . Può portare a molte cattive vulnerabilità.
Controllo dell'assunzione in fase di compilazione mediante enum: esempio stupido, ma può essere davvero utile per le librerie con costanti configurabili in fase di compilazione.
#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)
};
#define CompilerAssert(exp) extern char _CompilerAssert[(exp)?1:-1]
)
Gcc (c) ha alcune caratteristiche divertenti che puoi abilitare, come dichiarazioni di funzioni nidificate e la forma a?: B dell'operatore?: Che restituisce a se a non è falso.
Ho scoperto di recente 0 bitfield.
struct {
int a:3;
int b:2;
int :0;
int c:4;
int d:3;
};
che darà un layout di
000aaabb 0ccccddd
anziché senza: 0;
0000aaab bccccddd
Il campo di larghezza 0 indica che i seguenti campi di bit devono essere impostati sull'entità atomica successiva ( char
)
Macro argomento variabile in stile C99, aka
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "\n", \
__VAR_ARGS__)
quale sarebbe usato come
ERR(errCantOpen, "File %s cannot be opened", filename);
Qui uso anche l'operatore stringize e la concatentazione costante di stringhe, altre funzionalità che mi piacciono molto.
In alcuni casi sono utili anche variabili automatiche di dimensioni variabili. Questi sono stati aggiunti in nC99 e sono supportati da molto tempo in gcc.
void foo(uint32_t extraPadding) {
uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
Si finisce con un buffer nello stack con spazio per l'intestazione del protocollo di dimensioni fisse più i dati di dimensioni variabili. Puoi ottenere lo stesso effetto con alloca (), ma questa sintassi è più compatta.
Devi assicurarti che extraPadding sia un valore ragionevole prima di chiamare questa routine, o finisci per far esplodere lo stack. Dovresti controllare la correttezza degli argomenti prima di chiamare malloc o qualsiasi altra tecnica di allocazione della memoria, quindi non è davvero insolito.