Ukryte funkcje C


141

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.


Byłoby wspaniale, gdybyś ty / ktoś miał edytować „pytanie”, aby wskazać najlepsze ukryte funkcje, takie jak w wersjach C # i Perl tego pytania.
Donal Fellows

Odpowiedzi:


62

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.


29
Ale gdyby określili zachowanie przy przepełnieniu, spowodowałoby to bardzo powolne działanie na architekturach, w których nie było to normalne zachowanie. Bardzo niskie narzuty czasu wykonania zawsze były celem projektowym C, co oznacza, że ​​wiele takich rzeczy jest nieokreślonych.
Mark Baker,

9
Doskonale zdaję sobie sprawę, dlaczego przepełnienie to UB. Wciąż jest to niedopasowanie, ponieważ standard powinien zawierać przynajmniej procedury biblioteczne, które mogą testować przepełnienie arytmetyczne (wszystkich podstawowych operacji) bez powodowania UB.
zvrba

2
@zvrba, „procedury biblioteczne, które mogą testować przepełnienie arytmetyczne (wszystkich podstawowych operacji)”, gdybyś to dodał, spowodowałbyś znaczny spadek wydajności dla wszelkich operacji arytmetycznych na liczbach całkowitych. ===== Studium przypadku Matlab specjalnie DODAWA funkcję kontrolowania przepełnienia liczb całkowitych do zawijania lub nasycania. I również rzuca wyjątek, gdy występuje przepełnienie ==> Wydajność operacji Matlab na liczbach całkowitych: BARDZO WOLNA. Mój własny wniosek: myślę, że Matlab to fascynujące studium przypadku, które pokazuje, dlaczego nie chcesz sprawdzać przepełnienia liczb całkowitych.
Trevor Boyd Smith

15
Powiedziałem, że standard powinien zapewnić bibliotekę obsługę do sprawdzania przepełnienia arytmetycznego. W jaki sposób procedura biblioteczna może spowodować wzrost wydajności, jeśli nigdy jej nie używasz?
zvrba

5
Dużym minusem jest to, że GCC nie ma flagi do wychwytywania przepełnień ze znakiem całkowitym i zgłaszania wyjątku czasu wykonywania. Chociaż istnieją flagi x86 do wykrywania takich przypadków, GCC ich nie wykorzystuje. Posiadanie takiej flagi pozwoliłoby aplikacjom, które nie mają krytycznego znaczenia dla wydajności (zwłaszcza starszym), czerpać korzyści z bezpieczeństwa przy minimalnym lub zerowym przeglądaniu kodu i refaktoryzacji.
Andrew Keeton

116

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();
     ...
}

2
Ta sztuczka jest fajna ... :) Zwłaszcza w przypadku zdefiniowanych przez Ciebie makr. :)
sundar - Przywróć Monikę

77
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!


3
Myślę, że to C99 lub coś takiego. Nie znalazłem przenośnego sposobu, aby upewnić się, że będą w pobliżu.
akauppi,

3
Są opcjonalną częścią C99, ale nie znam żadnych dostawców kompilatorów, którzy tego nie implementują.
Ben Collins,

10
stdint.h nie jest opcjonalne w C99, ale podążanie za standardem C99 najwyraźniej jest dla niektórych dostawców ( kaszel Microsoft).
Ben Combee,

5
@Pete, jeśli chcesz być analitykiem: (1) Ten wątek nie ma nic wspólnego z żadnym produktem Microsoft. (2) Ten wątek nigdy nie miał nic wspólnego z C ++. (3) Nie ma czegoś takiego jak C ++ 97.
Ben Collins

5
Spójrz na azillionmonkeys.com/qed/pstdint.h - prawie przenośny stdint.h
gnud Kwietnia

73

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.


7
W ciągu 20 lat CI NIGDY tego nie widziałem!
Martin Beckett

11
W C ++ możesz go nawet przeciążać.
Wouter Lievens

6
can! = powinien, oczywiście. Niebezpieczeństwo związane z przeładowaniem polega na tym, że wbudowane dotyczy już wszystkiego, w tym void, więc nigdy nie przestanie kompilować z powodu braku dostępnego przeciążenia. To znaczy, daje programistom dużo liny.
Aaron,

Int wewnątrz pętli nie będzie działać z C: jest to ulepszenie C ++. Czy "," jest tą samą operacją, co dla (i = 0, j = 10; i <j; j--, i ++)?
Aif

63

inicjalizacja struktury do zera

struct mystruct a = {0};

spowoduje to wyzerowanie wszystkich elementów konstrukcji.


2
Nie zeruje jednak wypełnienia, jeśli takie istnieje.
Mikeage

2
@simonn, nie, nie powoduje niezdefiniowanego zachowania, jeśli struktura zawiera typy niezintegrowane. memset z 0 w pamięci float / double nadal będzie równy zero, gdy interpretujesz zmiennoprzecinkowe / double (zmiennoprzecinkowe / podwójne są specjalnie zaprojektowane w ten sposób).
Trevor Boyd Smith

6
@Andrew: memset/ callocdo "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.
AnT

1
@nvl: Otrzymujesz fizyczne zero, gdy po prostu na siłę ustawisz całą pamięć zajmowaną przez obiekt na stan all-bits-zero. To właśnie memsetrobi ( 0jako 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 memsetna wskaźniku, otrzymasz 0x0000wskaźnik. Ale kiedy przypiszesz 0do wskaźnika, otrzymasz zerową wartość wskaźnika , która na poziomie fizycznym może być 0xBAADF00Dlub cokolwiek innego.
AnT

3
@nvl: Cóż, w praktyce różnica jest często tylko koncepcyjna. Ale teoretycznie może to mieć praktycznie każdy typ. Na przykład 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 dnie będą równe zero.
AnT

52

Stałe wieloznakowe:

int x = 'ABCD';

To ustawia się xna 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.


Jestem prawie pewien, że to nie jest przenośna konstrukcja. Wynik tworzenia stałej wieloznakowej jest określony przez implementację.
Mark Bessey

8
Komentarze „nieprzenośne” całkowicie mijają sedno sprawy. To tak, jakby krytykować program za używanie INT_MAX tylko dlatego, że INT_MAX jest „nieprzenośny” :) Ta funkcja jest tak przenośna, jak powinna. Stała wieloznakowa to niezwykle przydatna funkcja, która zapewnia czytelny sposób generowania unikalnych identyfikatorów całkowitych.
AnT

1
@Chris Lutz - Jestem prawie pewien, że końcowy przecinek wraca do K&R. Jest to opisane w drugim wydaniu (1988).
Ferruccio

1
@Ferruccio: Musisz myśleć o końcowym przecinku na zagregowanych listach inicjalizatora. Jeśli chodzi o końcowy przecinek w deklaracjach wyliczeniowych - jest to niedawny dodatek, C99.
AnT

3
Zapomniałeś „HANG” lub „BSOD” :-)
JBRWilkinson

44

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) .


Włączone komentarze Aarona i leppie , dzięki chłopaki.


Połączenie struktur i unii jest jeszcze bardziej interesujące - w systemach wbudowanych lub w kodzie sterownika niskiego poziomu. Przykładem jest, gdy chcesz przeanalizować rejestry karty SD, możesz je odczytać za pomocą union (1) i odczytać za pomocą union (2), który jest strukturą pól bitowych.
ComSubVie

5
Pola bitowe nie są przenośne - kompilator może dowolnie wybierać, czy w Twoim przykładzie odnogom zostaną przydzielone 3 najbardziej znaczące bity, czy 3 najmniej znaczące.
zvrba,

3
Pola bitowe są przykładem tego, gdzie standard daje implementacjom tak dużą swobodę w sposobie ich wdrażania, że ​​w praktyce są one prawie bezużyteczne. Jeśli obchodzi Cię, ile bitów zajmuje wartość i jak jest przechowywana, lepiej użyj masek bitowych.
Mark Bessey

26
Pola bitowe są rzeczywiście przenośne, o ile traktujesz je jako elementy struktury, którymi są, a nie jako „fragmenty liczb całkowitych”. Rozmiar, a nie lokalizacja, ma znaczenie w systemie wbudowanym z ograniczoną pamięcią, ponieważ każdy bit jest cenny ... ale większość dzisiejszych programistów jest zbyt młoda, aby o tym pamiętać. :-)
Adam Liss

5
@Adam: lokalizacja może mieć znaczenie w systemie wbudowanym (lub w innym miejscu), jeśli zależy od położenia pola bitowego w jego bajcie. Używanie masek usuwa wszelkie niejednoznaczności. Podobnie dla związków zawodowych.
Steve Melnikoff

37

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) .


18
C obsługuje tworzenie łańcuchów, więc możesz zrobić a ^ = b ^ = a ^ = b;
Dz.

4
Ściśle mówiąc, przykład państwowy jest zaznaczeniem preprocesora, a nie języka C - można używać tego pierwszego bez drugiego.
Greg Whitfield,

15
OJ: właściwie to, co sugerujesz, to niezdefiniowane zachowanie z powodu reguł dotyczących punktów sekwencji. Może działać na większości kompilatorów, ale nie jest poprawny ani przenośny.
Evan Teran,

5
Wymiana Xor mogłaby być faktycznie mniej wydajna w przypadku wolnego rejestru. Każdy przyzwoity optymalizator sprawiłby, że zmienna tymczasowa byłaby rejestrem. W zależności od implementacji (i potrzeby obsługi równoległości) wymiana może w rzeczywistości używać pamięci rzeczywistej zamiast rejestru (co byłoby takie samo).
Paul de Vrieze,

27
proszę, nigdy nie rób tego: en.wikipedia.org/wiki/…
Christian Oudard

37

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);
    }
}

29
@ComSubVie, każdy, kto używa urządzenia Duffa, to dzieciak ze skryptów, który zobaczył urządzenie Duffa i pomyślał, że jego kod wyglądałby na 1337, gdyby użył urządzenia Duffa. (1.) Urządzenie Duffa nie zapewnia żadnego wzrostu wydajności na nowoczesnym procesorze, ponieważ nowoczesne procesory mają zerowe zapętlenie. Innymi słowy, jest to przestarzały fragment kodu. (2.) Nawet jeśli twój procesor nie oferuje pętli zerowych narzutów, prawdopodobnie będzie miał coś takiego jak przetwarzanie SSE / altivec / vector, które zawstydzi Twoje urządzenie Duffa, gdy użyjesz memcpy (). (3.) Czy wspomniałem o tym innym, że robienie memcpy () duff nie jest przydatne?
Trevor Boyd Smith

2
@ComSubVie, poznaj moją Fist-of-death ( en.wikipedia.org/wiki/… )
Trevor Boyd Smith

12
@Trevor: więc tylko skrypt dzieciaków program 8051 i mikrokontrolery PIC, prawda?
SF.

6
@Trevor Boyd Smith: Chociaż urządzenie Duffa wydaje się przestarzałe, wciąż jest to historyczna ciekawostka, która potwierdza odpowiedź ComSubVie. W każdym razie, cytując Wikipedię: „Kiedy wiele instancji urządzenia Duffa zostało usuniętych z serwera XFree86 w wersji 4.0, nastąpiła zauważalna poprawa wydajności”. ...
paercebal

2
W Symbianie ocenialiśmy kiedyś różne pętle do szybkiego kodowania pikseli; urządzenie duffa w asemblerze było najszybsze. Więc nadal ma to znaczenie dla głównych rdzeni ARM w twoich smartfonach.
Will

33

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.


Składnia gcc obsługiwana przez długi czas różni się od standardowej składni C99.
Mark Baker

28

C99 ma niesamowitą inicjalizację struktury w dowolnym porządku.

struct foo{
  int x;
  int y;
  char* name;
};

void main(){
  struct foo f = { .y = 23, .name = "awesome", .x = -38 };
}


27

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 ...


3
Ta funkcja jest zwykle nazywana „literałami złożonymi”. Struktury anonimowe (lub nienazwane) wyznaczają zagnieżdżone struktury, które nie mają nazw elementów członkowskich.
calandoa

zgodnie z moim GCC, „ISO C90 zabrania literałów złożonych”.
jmtd

„ISO C99 obsługuje złożone literały”. „Jako rozszerzenie, GCC obsługuje złożone literały w trybie C89 i C ++” (dixit info gcc). Dodatkowo, „Jako rozszerzenie GNU, GCC umożliwia inicjowanie obiektów ze statycznym czasem trwania za pomocą literałów złożonych (co nie jest możliwe w ISO C99, ponieważ inicjator nie jest stałą)”.
PypeBros

24

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)));

24

(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.


24

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.


Ale w rzeczywistości, jak często musisz kompilować swój kod za pomocą innego kompilatora?
Joe D

3
@Joe D, jeśli jest to projekt wieloplatformowy, taki jak Windows / OSX / Linux, prawdopodobnie trochę, a także istnieje inny arch, taki jak x86 vs x86_64 i itp ...
Pharaun

@JoeD Chyba że jesteś w bardzo wąskim projekcie, który jest szczęśliwy mogąc poślubić jednego dostawcę kompilatora, bardzo. Możesz chcieć uniknąć faktycznej zmiany kompilatorów, ale nie chcesz, aby ta opcja była otwarta. Jednak w przypadku systemów wbudowanych nie zawsze masz wybór. AHS, DUP.
XTL

19

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);

16

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:

  • trzy razy dłużej
  • znacznie trudniejsze do odczytania
  • znacznie wolniej
  • mniej wydajne przy ustawieniu dowolnego limitu rozmiaru bufora (ale musiałbyś użyć nawet dłuższego kodu, aby tego uniknąć bez ciągłej kontenacji ciągów).
  • zajmują więcej miejsca w stosie

1
Przydatne jest również dzielenie stałej łańcuchowej na wiele linii źródłowych bez używania brudnego `\`.
dolmen,

15

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 .


12

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.


Jest to również ważne w środowisku wielu programistów, w którym na przykład Eric dodaje „baz”, a następnie George dodaje „boom”. Jeśli Eric zdecyduje się wyciągnąć swój kod do następnej kompilacji projektu, nadal kompiluje się on ze zmianą George'a. Bardzo ważne w przypadku kontroli kodu źródłowego wielu branż i nakładających się harmonogramów rozwoju.
Harold Bamford

Wyliczenia mogą być C99. Inicjatory tablicy i kończący przecinek to K&R.
Ferruccio,

Zwykłe wyliczenia znajdowały się w c89, AFAIK. Przynajmniej są w pobliżu od wieków.
XTL,

12

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.


4
Oczywiście omijanie dużych struktur w ten sposób ma wpływ na wydajność; często jest to przydatne (i rzeczywiście jest coś, o czym wiele osób nie zdaje sobie sprawy, że możesz to zrobić), ale musisz rozważyć, czy mijanie wskaźników jest lepsze.
Mark Baker,

1
Oczywiście, że tak. Kompilator nie może również wykryć użycia i zoptymalizować go.
zrelaksuj się

Uważaj, jeśli którykolwiek z elementów jest wskaźnikami, ponieważ będziesz kopiować same wskaźniki, a nie ich zawartość. Oczywiście to samo dotyczy memcpy ().
Adam Liss

Kompilator nie może zoptymalizować tego konwertowania według wartości za pomocą by-referenece, chyba że może wykonywać globalne optymalizacje.
Blaisorblade

Prawdopodobnie warto zauważyć, że w C ++ standard specjalnie umożliwia optymalizację kopii (standard musi zezwalać kompilatorom na implementację, ponieważ oznacza to, że konstruktor kopiujący, który może mieć efekty uboczne, nie może być wywoływany), a ponieważ większość kompilatorów C ++ są także kompilatorami C, istnieje duża szansa, że ​​Twój kompilator przeprowadzi tę optymalizację.
Joseph Garvin,

10

Dziwne indeksowanie wektorów:

int v[100]; int index = 10; 
/* v[index] it's the same thing as index[v] */

4
Jest jeszcze lepiej ... char c = 2 ["Hello"]; (c == 'l' po tym).
rok

5
Nie takie dziwne, biorąc pod uwagę, że v [index] == * (v + index) i index [v] == * (index + v)
Ferruccio

17
Proszę, powiedz mi, że tak naprawdę nie używasz tego „cały czas”, jak zadaje pytanie!
Tryke,

9

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ą:

  1. Poleganie na tym, jak kompilator układa struktury w pamięci.
  2. Założenia dotyczące endianizmu liczb całkowitych / .
  3. Założenia dotyczące funkcji ABI.
  4. Założenia dotyczące kierunku wzrostu ramek stosu.
  5. Założenia dotyczące kolejności realizacji w instrukcjach.
  6. Założenia dotyczące kolejności wykonywania instrukcji w argumentach funkcji.
  7. Założenia dotyczące rozmiaru bitu lub precyzji typów short, int, long, float i double.

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”.


Aby rozwiązać większość z nich, uzależnij te założenia od cech swojej platformy i opisz każdą platformę w jej własnym nagłówku. Realizacja zamówienia jest wyjątkiem - nigdy na tym nie polegaj; w przypadku innych pomysłów każda platforma musi mieć rzetelną decyzję.
Blaisorblade,

2
@Blaisorblade, Jeszcze lepiej, użyj asercji w czasie kompilacji, aby udokumentować swoje założenia w sposób, który spowoduje niepowodzenie kompilacji na platformie, na której są naruszane.
RBerteig

Myślę, że należy połączyć oba, aby twój kod działał na wielu platformach (taki był pierwotny zamiar), a jeśli makra funkcji są ustawione w niewłaściwy sposób, asercje w czasie kompilacji złapią to. Nie jestem pewien, czy, powiedzmy, założenia dotyczące ABI funkcji są sprawdzalne jako asercje w czasie kompilacji, ale powinno być możliwe dla większości innych (ważnych) (z wyjątkiem kolejności wykonywania ;-)).
Blaisorblade

Testy funkcji ABI powinny być obsługiwane przez zestaw testów.
dolmen

9

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

8

używanie INT (3) do ustawiania punktu przerwania w kodzie jest moim ulubionym przez cały czas


3
Nie sądzę, że jest przenośny. Będzie działać na x86, ale co z innymi platformami?
Cristian Ciupitu

1
Nie mam pojęcia - powinieneś zadać pytanie na ten temat
Dror Helper

2
To dobra technika i jest specyficzna dla X86 (chociaż prawdopodobnie są podobne techniki na innych platformach). Jednak nie jest to cecha C. Zależy to od niestandardowych rozszerzeń C lub wywołań bibliotek.
Ferruccio

1
W GCC jest __builtin_trap, a dla MSVC __debugbreak, które będą działać na każdej obsługiwanej architekturze.
Axel Gneiting

8

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.


link już nie działa, w rzeczywistości wydaje się, że sama strona nie działa. Czy możesz podać inny link?
thequark,

@thequark: Każdy artykuł na temat „luk w zabezpieczeniach ciągu formatu” będzie zawierał pewne informacje ... (np. crypto.stanford.edu/cs155/papers/formatstring-1.2.pdf ). Jednak ze względu na charakter tego pola zabezpieczenia same strony internetowe są trochę niestabilne i trudno o prawdziwe artykuły naukowe (po wdrożeniu).
Sridhar Iyer

8

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)
};

2
+1 schludnie. Kiedyś używałem makra CompilerAssert firmy Microsoft, ale twoje też nie jest złe. ( #define CompilerAssert(exp) extern char _CompilerAssert[(exp)?1:-1])
Patrick Schlüter

1
Podoba mi się metoda wyliczania. Podejście, które zastosowałem wcześniej, wykorzystywało eliminację martwego kodu: „if (something_bad) {void BLORG_IS_WOOZLED (void); BLORG_IS_WOOZLED ();}”, które nie powodowało błędów do czasu połączenia, chociaż dawało tę zaletę, że pozwalało programista wie poprzez komunikat o błędzie, że błąd został rozwiązany.
supercat

8

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.


8

Ostatnio odkryłem 0 pól bitowych.

struct {
  int    a:3;
  int    b:2;
  int     :0;
  int    c:4;
  int    d:3;
};

co da układ

000aaabb 0ccccddd

zamiast bez: 0;

0000aaab bccccddd

Pole o szerokości 0 mówi, że następujące pola bitowe powinny być ustawione na następnej atomowej encji ( char)


7

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ę.


Masz dodatkowe „R” w VA_ARGS .
Blaisorblade,

6

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.


Czy to również zadziała poprawnie, jeśli bajt / znak nie ma dokładnie 8 bitów szerokości na platformie docelowej? Wiem, te przypadki są rzadkie, ale nadal ... :)
Stephan202
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.