Wiem, że za wszystkimi implementacjami kompilatorów C stoi standard, więc nie powinno być żadnych ukrytych funkcji. Mimo to jestem pewien, że wszyscy programiści C mają ukryte / tajne sztuczki, których używają cały czas.
Wiem, że za wszystkimi implementacjami kompilatorów C stoi standard, więc nie powinno być żadnych ukrytych funkcji. Mimo to jestem pewien, że wszyscy programiści C mają ukryte / tajne sztuczki, których używają cały czas.
Odpowiedzi:
Wskaźniki funkcji. Możesz użyć tabeli wskaźników funkcji do implementacji, np. Szybkich interpreterów kodu z pośrednią wątkami (FORTH) lub dyspozytorów kodu bajtowego, lub do symulacji metod wirtualnych podobnych do obiektów obiektowych.
Następnie w bibliotece standardowej znajdują się ukryte perełki, takie jak qsort (), bsearch (), strpbrk (), strcspn () [dwa ostatnie są przydatne do implementacji zamiany strtok ()].
Nieprawidłowością języka C jest to, że przepełnienie arytmetyczne ze znakiem jest niezdefiniowanym zachowaniem (UB). Więc ilekroć zobaczysz wyrażenie, takie jak x + y, oba są podpisanymi intami, może to potencjalnie spowodować przepełnienie i spowodować UB.
Bardziej sztuczka kompilatora GCC, ale możesz dać kompilatorowi wskazówki dotyczące rozgałęzienia (powszechne w jądrze Linuksa)
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
zobacz: http://kerneltrap.org/node/4705
Podoba mi się to, że dodaje też wyrazistości niektórym funkcjom.
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
Są to opcjonalne elementy w standardzie, ale muszą to być cechy ukryte, ponieważ ludzie ciągle je przedefiniowują. Jedna baza kodu, nad którą pracowałem (i nadal robię) ma wiele redefinicji, wszystkie z różnymi identyfikatorami. W większości przypadków jest to z makrami preprocesora:
#define INT16 short
#define INT32 long
I tak dalej. To sprawia, że chcę wyrywać sobie włosy. Po prostu użyj cholernych standardowych typów całkowitych!
Operator przecinka nie jest powszechnie używany. Z pewnością można go nadużywać, ale może też być bardzo przydatne. To zastosowanie jest najbardziej powszechne:
for (int i=0; i<10; i++, doSomethingElse())
{
/* whatever */
}
Ale możesz użyć tego operatora wszędzie. Przestrzegać:
int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
Każda instrukcja jest oceniana, ale wartością wyrażenia będzie wartość ostatniej ocenianej instrukcji.
inicjalizacja struktury do zera
struct mystruct a = {0};
spowoduje to wyzerowanie wszystkich elementów konstrukcji.
memset
/ calloc
do "wszystkie bajty zero" (tj. Fizyczne zera), co rzeczywiście nie jest zdefiniowane dla wszystkich typów. { 0 }
gwarantuje, że wszystko zostanie zintilizowane odpowiednimi logicznymi wartościami zerowymi. Na przykład wskaźniki są gwarantowane, aby uzyskać ich prawidłowe wartości null, nawet jeśli wartość null na danej platformie to 0xBAADFOOD
.
memset
robi ( 0
jako drugi argument). Logiczne zero uzyskuje się po zainicjowaniu / przypisaniu 0
(lub { 0 }
) do obiektu w kodzie źródłowym. Te dwa rodzaje zer niekoniecznie dają ten sam wynik. Jak w przykładzie ze wskaźnikiem. Kiedy robisz memset
na wskaźniku, otrzymasz 0x0000
wskaźnik. Ale kiedy przypiszesz 0
do wskaźnika, otrzymasz zerową wartość wskaźnika , która na poziomie fizycznym może być 0xBAADF00D
lub cokolwiek innego.
double
. Zwykle jest realizowany zgodnie ze standardem IEEE-754, w którym zero logiczne i zero fizyczne są takie same. Jednak IEEE-754 nie jest wymagany przez język. Może się więc zdarzyć, że kiedy to zrobisz double d = 0;
(logiczne zero), fizycznie niektóre bity w pamięci d
nie będą równe zero.
Stałe wieloznakowe:
int x = 'ABCD';
To ustawia się x
na 0x41424344
(lub 0x44434241
, w zależności od architektury).
EDYCJA: Ta technika nie jest przenośna, zwłaszcza jeśli serializujesz int. Jednak tworzenie samodokumentujących się wyliczeń może być niezwykle przydatne. na przykład
enum state {
stopped = 'STOP',
running = 'RUN!',
waiting = 'WAIT',
};
To sprawia, że jest to znacznie prostsze, jeśli patrzysz na surowy zrzut pamięci i potrzebujesz określić wartość wyliczenia bez konieczności jej wyszukiwania.
Nigdy nie używałem pól bitowych, ale brzmią fajnie dla rzeczy o bardzo niskim poziomie.
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;
}
Oznacza to, że sizeof(cat)
może być tak mały, jaksizeof(char)
.
C ma standard, ale nie wszystkie kompilatory C są w pełni zgodne (nie widziałem jeszcze żadnego w pełni zgodnego kompilatora C99!).
To powiedziawszy, sztuczki, które preferuję, są nieoczywiste i przenośne na różnych platformach, ponieważ opierają się na semantyce C. Zwykle dotyczą makr lub arytmetyki bitowej.
Na przykład: zamiana dwóch liczb całkowitych bez znaku bez użycia zmiennej tymczasowej:
...
a ^= b ; b ^= a; a ^=b;
...
lub „rozszerzające C” do reprezentowania maszyn skończonych, takich jak:
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
można to osiągnąć za pomocą następujących makr:
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
Ogólnie jednak nie lubię sztuczek, które są sprytne, ale sprawiają, że kod jest niepotrzebnie skomplikowany do odczytania (jak przykład wymiany) i uwielbiam te, które sprawiają, że kod jest jaśniejszy i bezpośrednio przekazuje intencję (jak przykład FSM) .
Struktury z przeplotem, takie jak Urządzenie Duffa :
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);
}
}
Bardzo lubię wyznaczone inicjatory, dodane w C99 (i obsługiwane w gcc przez długi czas):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
Inicjalizacja tablicy nie jest już zależna od pozycji. Jeśli zmienisz wartości FOO lub BAR, inicjalizacja tablicy automatycznie odpowiada ich nowej wartości.
anonimowe struktury i tablice to moja ulubiona. (por. http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html )
setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));
lub
void myFunction(type* values) {
while(*values) x=*values++;
}
myFunction((type[]){val1,val2,val3,val4,0});
może być nawet używany do tworzenia połączonych list ...
gcc ma wiele rozszerzeń języka C, które mi się podobają, które można znaleźć tutaj . Niektóre z moich ulubionych to atrybuty funkcji . Niezwykle przydatnym przykładem jest atrybut format. Można tego użyć, jeśli zdefiniujesz funkcję niestandardową, która przyjmuje ciąg formatu printf. Jeśli włączysz ten atrybut funkcji, gcc sprawdzi twoje argumenty, aby upewnić się, że łańcuch formatu i argumenty pasują do siebie i odpowiednio wygeneruje ostrzeżenia lub błędy.
int my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));
(ukryta) funkcja, która "zszokowała" mnie, kiedy pierwszy raz zobaczyłem, dotyczy printf. ta funkcja umożliwia używanie zmiennych do formatowania samych specyfikatorów formatu. poszukaj kodu, zobaczysz lepiej:
#include <stdio.h>
int main() {
int a = 3;
float b = 6.412355;
printf("%.*f\n",a,b);
return 0;
}
znak * osiąga ten efekt.
Cóż ... Myślę, że jedną z mocnych stron języka C jest jego przenośność i standardowość, więc ilekroć znajdę jakąś "ukrytą sztuczkę" w implementacji, z której obecnie korzystam, staram się jej nie używać, ponieważ staram się zachować Kod C jako standardowy i przenośny, jak to możliwe.
Asercje w czasie kompilacji, jak już omówiono tutaj .
//--- 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);
Stała konkatenacja ciągów
Byłem dość zaskoczony, że nie widziałem tego już w odpowiedziach, ponieważ wszystkie kompilatory, które znam, obsługują to, ale wielu programistów wydaje się to ignorować. Czasami jest to bardzo przydatne i nie tylko podczas pisania makr.
Przypadek użycia, który mam w moim obecnym kodzie: mam #define PATH "/some/path/"
w pliku konfiguracyjnym (tak naprawdę jest to ustawiane przez makefile). Teraz chcę zbudować pełną ścieżkę, w tym nazwy plików, do otwierania zasobów. Po prostu trafia do:
fd = open(PATH "/file", flags);
Zamiast okropnego, ale bardzo powszechnego:
char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
Zauważ, że powszechne okropne rozwiązanie to:
Cóż, nigdy go nie używałem i nie jestem pewien, czy kiedykolwiek poleciłbym go komukolwiek, ale czuję, że to pytanie byłoby niepełne bez wzmianki o wspólnej sztuczce Simona Tathama .
Podczas inicjowania tablic lub wyliczeń można umieścić przecinek po ostatnim elemencie na liście inicjalizacyjnej. na przykład:
int x[] = { 1, 2, 3, };
enum foo { bar, baz, boom, };
Zrobiono to tak, że jeśli generujesz kod automatycznie, nie musisz się martwić o usunięcie ostatniego przecinka.
Przypisanie struktury jest fajne. Wiele osób nie zdaje sobie sprawy, że struktury są również wartościami i można je przypisać, nie ma potrzeby ich używaćmemcpy()
, gdy proste przypisanie .
Na przykład rozważmy pewną wyimaginowaną bibliotekę graficzną 2D, która może zdefiniować typ reprezentujący (całkowitą) współrzędną ekranu:
typedef struct {
int x;
int y;
} Point;
Teraz robisz rzeczy, które mogą wyglądać „źle”, takie jak napisanie funkcji, która tworzy punkt zainicjowany z argumentów funkcji i zwraca go, na przykład:
Point point_new(int x, int y)
{
Point p;
p.x = x;
p.y = y;
return p;
}
Jest to bezpieczne, o ile (oczywiście) wartość zwracana jest kopiowana przez wartość przy użyciu przypisania struktury:
Point origin;
origin = point_new(0, 0);
W ten sposób możesz napisać całkiem czysty i zorientowany obiektowo kod, wszystko w prostym standardzie C.
Dziwne indeksowanie wektorów:
int v[100]; int index = 10;
/* v[index] it's the same thing as index[v] */
Kompilatory C implementują jeden z kilku standardów. Jednak posiadanie standardu nie oznacza, że wszystkie aspekty języka są zdefiniowane. Urządzenie DuffaNa przykład jest ulubioną „ukrytą” funkcją, która stała się tak popularna, że współczesne kompilatory mają specjalny kod rozpoznający, aby zapewnić, że techniki optymalizacji nie zakłócą pożądanego efektu tego często używanego wzorca.
Ogólnie rzecz biorąc, ukryte funkcje lub sztuczki językowe są odradzane, ponieważ pracujesz na skraju dowolnego standardu C, którego używa twój kompilator. Wiele takich sztuczek nie działa od jednego kompilatora do drugiego i często tego rodzaju funkcje zawodzą z jednej wersji zestawu kompilatorów danego producenta do innej wersji.
Różne sztuczki, które złamały kod C, obejmują:
Inne problemy i problemy, które pojawiają się, gdy programiści przyjmują założenia dotyczące modeli wykonania, które są określone w większości standardów języka C jako zachowanie „zależne od kompilatora”.
Podczas korzystania z sscanf możesz użyć% n, aby dowiedzieć się, gdzie powinieneś kontynuować czytanie:
sscanf ( string, "%d%n", &number, &length );
string += length;
Najwyraźniej nie możesz dodać kolejnej odpowiedzi, więc dołączę tutaj drugą, możesz użyć „&&” i „||” jako warunkowe:
#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 );
}
Ten kod wyświetli:
cześć ROFL
używanie INT (3) do ustawiania punktu przerwania w kodzie jest moim ulubionym przez cały czas
Moją ulubioną "ukrytą" cechą C jest użycie% nw printf do zapisu z powrotem na stosie. Zwykle printf zdejmuje wartości parametrów ze stosu na podstawie łańcucha formatującego, ale% n może je z powrotem zapisać.
Sprawdź sekcję 3.4.2 tutaj . Może prowadzić do wielu nieprzyjemnych luk w zabezpieczeniach.
Sprawdzanie założeń w czasie kompilacji przy użyciu wyliczeń: głupi przykład, ale może być naprawdę przydatne w bibliotekach ze stałymi konfigurowalnymi w czasie kompilacji.
#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) ma kilka fajnych funkcji, które możesz włączyć, takie jak deklaracje funkcji zagnieżdżonych i postać a?: B operatora?:, Który zwraca a, jeśli a nie jest fałszem.
Makra argumentów zmiennych w stylu C99, aka
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "\n", \
__VAR_ARGS__)
który byłby używany jak
ERR(errCantOpen, "File %s cannot be opened", filename);
Tutaj również używam operatora stringize i ciągłej konkatentacji ciągów, innych funkcji, które naprawdę lubię.
W niektórych przypadkach przydatne są również zmienne automatyczne o zmiennej wielkości. Zostały one dodane w nC99 i przez długi czas były obsługiwane w gcc.
void foo(uint32_t extraPadding) {
uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
Otrzymujesz bufor na stosie z miejscem na nagłówek protokołu o stałym rozmiarze oraz dane o zmiennym rozmiarze. Możesz uzyskać ten sam efekt za pomocą funkcji przydzielania (), ale ta składnia jest bardziej zwarta.
Musisz upewnić się, że extraPadding jest rozsądną wartością przed wywołaniem tej procedury, w przeciwnym razie zdmuchniesz stos. Przed wywołaniem malloc lub innej techniki alokacji pamięci musiałbyś sprawdzić argumenty, więc nie jest to naprawdę niezwykłe.