Ciekawostki w C

W tym miejscu zadajemy pytania na temat języka C, dzielimy się swoją wiedzą, udzielamy wsparcia, rozwiązujemy problemy programistyczne.
Awatar użytkownika
mokrowski
User
User
Posty: 190
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Ciekawostki w C

Postautor: mokrowski » sobota 02 kwie 2016, 13:43

Przeszukując swoje przepastne zasoby backupów, znalazłem troszkę „fistaszków” dla języka C. Fistaszkami nazywam luźne uwagi i wyjątki z kodu który kiedyś widziałem, zachwyciłem się lub który mnie zdziwił. Pozostawię je w takiej postaci jak są. Z góry przepraszam za brak wszystkich odnośników do zasobów sieciowych. Szukanie miało by sens gdyby miała powstać z tego książka. Jestem więc pewien że ktoś/gdzieś na to wpadł. Chętni i tak pogłębią swoją wiedzę i poszukają a niechętni będą szukali powodu dlaczego tego nie zrobili. Daleki jestem od stwierdzenia „stosuj to i się nie oglądaj". Część z tych technik nie jest dobrze widzianych w standardowych programach. Niemniej jednak warto je znać aby nie zostać zaskoczonym lub użyć jeśli będą ku temu uzasadnione okoliczności.

Czasem będę używał skrótu UB co oznacza Undefined Behavior. W bardzo wielkim skrócie to oznacza że w standardzie języka zauważono stosowanie takich konstrukcji ale świadomie nie definiuje się zachowania kompilatora ze względu na wpływ na wydajność kodu tworzonego przez tenże kompilator. Tworząc oprogramowanie masz po prostu wiedzieć że... tak się nie robi. Stosując UB, ryzykujesz że organizacja znana z historii pod tym akronimem wpadnie oknem, wyrwie Ci głowę i zawoła do pustego korpusu „czy wiesz co robisz?" :-/
Tak, wiem że to makabryczny żart ale naprawdę może zdarzyć się wszystko. Włącznie z usunięciem danych z dysku.
To przeczytasz co oznacza UB.

https://en.wikipedia.org/wiki/Undefined_behavior

Jeśli jakieś zagadnienie spotka się z powszechnym zaciekawieniem, pokażę jak można użyć sensownie daną technikę. A jeśli nie.. cóż.. przynajmniej nie zaginie :-)

Punkty sekwencyjne jako element działania języka oraz częste źródło UB

Język C uzgadnia stan zmiennych (czyli inaczej tzw. efekty uboczne) wyłącznie w określonych punktach. Robi to ze względu na wolność pozostawioną twórcom kompilatora. Umożliwia to implementację kompilatora wydajnie na danej platformie.

https://en.wikipedia.org/wiki/Sequence_point

Kod: Zaznacz cały

int i = 0;
int f(int x) {
    return x + i;
}
int x = f(i++);
int y = (i++) + i;


y jest niezdefiniowane bo zależy od efektów ubocznych (inkrementacji) która w momencie wywołania nie jest znana. x jest zdefiniowane bo nie zależy od tych efektów.

setjmp() i longimp() jako implementacja nielokalnego skoku oraz możliwość implementacji współprogramów, wyjątków i kontynuacji

To naprawdę potężny mechanizm wymagający oddzielnego omówienia.

http://en.m.wikipedia.org/wiki/Setjmp.h
https://en.wikipedia.org/wiki/Continuation

Użycie specyfikatora %n w printf()

Kod: Zaznacz cały

#include <stdio.h>                                                                                           
int main() {                                                                                                 
    int count;                                                                                               
    printf("Witamy%n na forum Microgeek!\n", &count);                                                                       
    printf("Słowo \"Witamy\" zawiera %d znaków.\n", count);                                           
}


Przy prawidłowym działaniu zwraca:

Kod: Zaznacz cały

Witamy na forum Microgeek!
Słowo "Witamy" zawiera 6 znaków.


Inne zastosowanie, liczenie ilości znaków wprowadzonych:

Kod: Zaznacz cały

sscanf(string, "%d%n", &number, &length );


Czasem warto zastosować dla nieznanej ilości znaków w ciągu:

Kod: Zaznacz cały

int pos1, pos2;
char *string_of_unknown_length = ”nie mam zielonego pojęcia ile ten napis ma znaków";
printf(”Oto napis: %n(%s)%n.\n”, &pos1, string_of_unknown_length, &pos2);
printf("%*s", pos1+1, " ");
for(int i=pos1+1; i<pos2-1; i++) {
    putc('-', stdout);
}
putc('\n', stdout);


W wyniku ładnie podkreślony napis.

Funkcje biblioteki standardowej które są mniej znane

qsort(), bsearch(), strpbrk(), strcspn(). Ostatnie dwie mogą zastąpić strtok().
Zapoznaj się i napisz przykład a zobaczysz że się przyda.

Czytanie skomplikowanych typów od prawej do lewej

http://edu.pjwstk.edu.pl/wyklady/pro/scb/PRG2CPP_files/node33.html
http://ieng9.ucsd.edu/~cs30x/rt_lt.rule.html

Kod: Zaznacz cały

int a[5]; /* a[0], ..., a[4] to tablica 5 elementów typu int */
int (*pa)[5]; /* (*pa)[0], ..., (*pa)[4] to wskaźnik na tablicę 5 elementów typu int */
int *ap[5]; /* *ap[0], .., *ap[4] to tablica 5 elementów typu wskaźnik na typ int */
int *p; /* *p to wskaźnik na typ int */


Jawna inicjalizacja (C99)

Kod: Zaznacz cały

struct Foo {
    int x;
    int y;
    int z;
};
struct Foo foo = {.z = 3, .x = 5};


Ta sama składnia obowiązuje także dla tablic. Jeśli nie wymienisz jawnie indeksu, element pod tymże zostanie wyzerowany.

Kod: Zaznacz cały

int a[5] = {[1] = 2, [4] = 5};
int a[] = {[1] = 2, [4] = 5};
int a[5] = {0, 2, 0, 0, 5};


W połączeniu można tego użyć tak:

Kod: Zaznacz cały

#include <stdio.h>
typedef struct {
    int a;
    double b;
} s_t;
int main(void) {
    s_t values[5] = {
        [0] = { .a = 1, .b = 13.2 },
        [1] = { 10, 10.0 },
        [3] = { .a = 4, .b = 45.1 }
    };
    printf("%d %f\n", values[1].a, values[1].b);
}


Wskaźniki z restricted (C99)

Słowo kluczowe restricted może być zastosowane dla wskaźnika przekazywanego do funkcji. Oznacza ono że dostęp do wartości na który wskazuje wskaźnik, będzie odbywał się wyłącznie z użyciem tegoż wskaźnika. Jeśli na wartość wskazuje także inny wskaźnik, standard C99 mówi o UB.

Kod: Zaznacz cały

int f(const int* restrict x, int* y) {
    (*y)++;
    int z = *x;
    (*y)--;
    return z;
}

.. informujemy kompilator że zmienne x i y nigdy nie będą wskazywały na tę samą przestrzeń danych. Choć to naiwny przykład to kompilator zoptymalizować może to do:

Kod: Zaznacz cały

int f(const int* restrict x, int* y) {
    return *x;
}


Jeszcze raz, jeśli x i y jako wskaźniki wskazują na ten sam obszar pamięci, mamy do czynienia z UB!

Kombinacja działania const i volatile

http://embeddedgurus.com/barr-code/2012/01/combining-cs-volatile-and-const-keywords/

Statyczne i jawne parametry tablicy (C99)

Kod: Zaznacz cały

void f(int a[static 10]) {
    /* ... */
}


Jest obietnicą że przesłana tablica zawierać będzie co najmniej 10 elementów. Jeśli będzie mniej lub null i będzie to możliwe do określenia na etapie kompilacji, kompilator zgłosi ostrzeżenie. Dodatkowo kompilator może poczynić optymalizacje że tablica nigdy nie będzie pusta (is not null).

Kod: Zaznacz cały

void f(int a[const]) {
    /* ... */
}


.. obiecujemy nie modyfikować wskaźnika a. To taki sam efekt jak: f(int * const a) {... }.
W połączeniu ze static, uzyskujemy jednak coś czego nie da się inaczej jawnie zapisać:

Kod: Zaznacz cały

f(int a[static const 10]){ ... }


Nie dość że dane są stałe, to jeszcze nie będzie ich mniej niż 10.
Taki sam zapis obowiązuje także dla restrict i kombinacji z poprzednimi dwoma.

Wyrażenia generyczne (C11)

Umożliwiają wybór definiowanego elementu w zależności od typu przekazanych danych. Najczęściej występuje w makrach choć działa także poza nimi. Nie znaczy to oczywiście że C umożliwia przeciążanie funkcji.
od C11 umożliwia jedynie wybór co jest definiowane.

Kod: Zaznacz cały

#define cbrt(X) _Generic((X), \
                        long double: cbrtl, \
                        default: cbrt, \
                        float: cbrtf \
                        )(X)


Wywołanie cbrt(expr) jest przekładane na cbrtl(expr) jeśli wyrażenie expr jest typu long double, cbrtf jeśli jest typu float i cbrt w innych przypadkach.

Sposób na "stringifikację" komend

Jeśli mamy kod który łączy polecenie lub komendę z funkcją lub wskaźnikiem funkcji do wykonania, np taki:

Kod: Zaznacz cały

struct command {
    char *name;
    void (*function_com) (void);
};
struct command commands[] = {
    { "show", show_command },
    { "exit", exit_command },
    ...
};


.. i widać regularność w nazewnictwie funkcji (tu: nazwa_command), można posłużyć się makrem łączącym jedno z drugim:

Kod: Zaznacz cały

#define COMMAND(NAME) { #NAME, NAME ## _command }
struct command commands[] = {
    COMMAND (show),
    COMMAND (exit),
    ...
};


Nigdy nie było mi to potrzebne ale jak chcesz to sprawdź :-)

Swoją drogą.. gdzie ja to znalazłem?

Kod: Zaznacz cały

#include <stdio.h>
#include <math.h>
int main() {
    printf("%.0f\n",pow(2,747));
    return 0;
}


Zwraca poprawną wartość:

Kod: Zaznacz cały

740298315191606967520227188330889966610377319868419938630605715764070011466206019559325413145373572325939050053182159998975553533608824916574615132828322000124194610605645134711392062011527273571616649243219599128195212771328


Preprocesor robi co do niego należy czyli … włącza

Kod: Zaznacz cały

#include <stdio.h>
void main()
{
#include "next_program.c"       
}


.. a w pliku next_program.c jest np. to:

Kod: Zaznacz cały

printf("Witam na forum Microgeek.eu!\n");


Często wykorzystuję tę technikę do włączania tablic wyliczonych w trakcie budowania oprogramowania.

Przekazywanie danych pomiędzy funkcjami

Kod: Zaznacz cały

#include <stdio.h>
void init_message(void) {
    char msg[250] = "Witam Państwa bardzo serdecznie w następnym odcinku"
        "serialu o dziwnych właściwościach języka C";
}
void show_message(void) {
    char msg[250];
    printf("%s\n", msg);
}
int main(void) {
    init_message();
    show_message();
}


No tak także można ale chyba każdy widzi jaki program staje się przez to "kruchy" :-)

Dodawanie dwóch liczb bez operatora dodawania

Funkcja printf(), zwraca ilość wyświetlanych znaków. Ten efekt można wykorzystać do sumowania znaków. Po co i dlaczego.. bo można :-)

Kod: Zaznacz cały

#include <stdio.h>
int add(int a,int b){
    if(a!=0&&b!=0)
        return printf("%*c%*c",a,'\r',b,'\r');
    else return a!=0?a:b;
}
int main(){
    int A = 0, B = 0;
    printf("Enter the two numbers to add\n");
    scanf("%d %d",&A,&B);
    printf("Required sum is %d",add(A,B));
}


Ten sam efekt można osiągnąć z użyciem operatorów bitowych i rekurencji:

Kod: Zaznacz cały

int add(int x, int y)
{
    if (y == 0) {
        return x;
    } else {
        return add( x ^ y, (x & y) << 1);
    }
}


Nietypowe użycie operatora warunkowego

Obecnie działa w C++. W C (clang, gcc) nie..

Tradycyjne użycie:

Kod: Zaznacz cały

x = (y < 0) ? 10 : 20;


.. a teraz ciekawostka:

Kod: Zaznacz cały

(y < 0 ? x : y) = 20;


Można i tak:

Kod: Zaznacz cały

(b ? trueCount : falseCount)++


Czy można zwrócić void?

Ano można..

Kod: Zaznacz cały

static void foo (void) { }
static void bar (void) {
    return foo(); // Tu zwracany jest typ void
}
int main (void) {
    bar();
}


Operator przecinka nie taki straszny

Przypominam: Operator przecinka wykonuje operacje od lewej do prawej ale sam zwraca efekt działania operacji najbardziej skrajnej prawej. Z racji słabszego wiązania niż =, stosowany często w nawiasach ().

Kilka propozycji użycia:

Kod: Zaznacz cały

for (int i=0; i<7; ++i, exec_fun())
{
  /* kod..  */
}
...
int z = (printf("Przypisanie do z\n"), get_val());
...


Operatory logiczne && (and) i || (or), wykonują operacje „leniwie"

Jak wiadomo jeśli 1 argument && nie jest prawdą, drugi nie jest już sprawdzany. Także jeśli pierwszy argument || jest prawdą, nie ma sensu sprawdzać drugi.

Fakt ten można wykorzystać do wykonywania kodu warunkowego:

Kod: Zaznacz cały

#include <stdio.h>
#include <stdbool.h>
bool true_show(void) {
    printf("true\n");
    return true;
}
bool false_show(void) {
    printf("false\n");
    return false;
}
int main(void) {
    printf("true_show() && false_show()\n");
    true_show() && false_show();
    printf("true_show() || false_show()\n");
    true_show() || false_show();
    printf("false_show() && true_show()\n");
    false_show() && true_show();
    printf("false_show() || true_show()\n");
    false_show() || true_show();
}


Po połączeniu z operatorem przecinka, daje to możliwość innego zapisania konstrukcji if(...) else if(...) else:

Kod: Zaznacz cały

#include <stdio.h>
#include <stdbool.h>
void old(void) {
    printf("Gratulujemy słusznego wieku!\n");
}
void adult(void) {
    printf("No to jesteś dorosły!\n");
}
void child(void) {
    printf("W świetle prawa jesteś dzieckiem!\n");
}
int main(void) {
    unsigned age;
    printf("Podaj wiek: ");
    scanf("%d", &age);
    /*
     * Odpowiednik:
     * if(age < 18) {
     *     child();
     * } else if(age < 80) {
     *     adult();
     * } else if(age >= 80) {
     *     old();
     * }
     */
    (void) (((age < 18) && (child(), true))
            || ((age < 80) && (adult(), true))
            || (age >= 80 && (old(), true)));
}


Inny przykład:

Kod: Zaznacz cały

#include <stdio.h>
int main()
{
   1 || puts("Siema\n");
   0 || puts("Co tam\n");
   1 && puts("Słychać\n");
   0 && puts("Misiu\n");
}


Niebezpieczeństwo stosowania ciągów formatujących

https://crypto.stanford.edu/cs155/paper ... ng-1.2.pdf

Szybkie zerowanie struktur

Kod: Zaznacz cały

struct mystruct a = { 0 };


Podobnie można także zerować tablice.

Wartość int może mieć przypisany napis

Kod: Zaznacz cały

int a = 'abcd';


W ten sposób można wykryć big/little endian.

Dla 32-bit x86 wynik to:

Kod: Zaznacz cały

0x61626364


Dzięki tej właściwości, enum może mieć lepszy opis:

Kod: Zaznacz cały

enum state {
    stopped = 'STOP',
    running = 'RUN!',
    waiting = 'WAIT',
};

Oczywiście ta technika nie jest przenośna na inne platformy gdzie int może mieć min 16 bitów.

Zamiana zmiennych z użyciem xor

Obecnie nie zaleca się tego sposobu. Lepiej użyć zmiennej pomocniczej. No ale znać warto bo to ulubiony sposób sprawdzania przez HR czy znasz C :-)

Kod: Zaznacz cały

a ^= b ^= a ^= b;


Duff's device

case/switch działa przekraczając blok kodu:

https://en.wikipedia.org/wiki/Duff%27s_device

Kod: Zaznacz cały

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


Implementacja współprogramów dzięki temu chwytowi. Ten pan jest twórcą putty :-)

http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html

Indeksowanie w tablicy

Tradycyjnie użyte table[x] i x[table], są sobie równoważnie bo: v[index] == *(v + index) oraz index[v] == *(index + v)

Kod: Zaznacz cały

#include <stdio.h>
#include <stdbool.h>
int main(void) {
    char * msg[] = {"false", "true"};
    int values[] = {0, 10, 20, 30, 40, 50, 60};
    bool value = true;
    unsigned index;
    printf("value = %s\n", msg[value]);
    printf("value = %s\n", value[msg]);
    // Oraz...
    printf("Podaj wartość od 0 do 5: ");
    scanf("%d", &index);
    printf("Wartość: %d * 10 = %d\n", index, index[values]);
}


Inne użycie:

Kod: Zaznacz cały

hex = "0123456789abcdef"[someNybble];
hex = someNybble["0123456789abcdef"];


Compound literals czyli złożone literały

http://en.cppreference.com/w/c/language/compound_literal
http://www.drdobbs.com/the-new-c-compound-literals/184401404

Kod: Zaznacz cały

#include <stdio.h>
void show_values_term_zero(int table[]) {
    size_t counter = 0;
    while(table[counter] != 0) {
        printf("%d\n", table[counter]);
        ++counter;
    }
}
int main(void) {
    show_values_term_zero((int[]){1,3, 5, 6,0});
}


Asercja statyczna

W trakcie kompilacji, enum jest przetwarzany:

Kod: Zaznacz cały

// Upewnienie się że VAL jest potęgą 2'ki i mniejsze niż MAX_VAL
#define VAL 4
#define MAX_VAL 64
enum CompileTimeCheck
{
    VAL_POW_2   = 1/(((VAL) & ((VAL) - 1)) == 0),
    VAL_LESS_64 = 1/((VAL) < (MAX_VAL))
};
int main(void) {
    ;
}


Pomysł polega na sprowokowaniu dzielenia przez 0.

Nie można tworzyć tablicy o wielkości ujemnej co można użyć w ten sposób:

Kod: Zaznacz cały

#ifdef __GNUC__
#define STATIC_ASSERT_HELPER(expr, msg) \
    (!!sizeof \ (struct { unsigned int STATIC_ASSERTION__##msg: (expr) ? 1 : -1; }))
#define STATIC_ASSERT(expr, msg) \
    extern int (*assert_function__(void)) [STATIC_ASSERT_HELPER(expr, msg)]
#else
    #define STATIC_ASSERT(expr, msg)   \
    extern char STATIC_ASSERTION__##msg[1]; \
    extern char STATIC_ASSERTION__##msg[(expr)?1:2]
#endif /* __GNUC__ */


Makro upraszczające debugowanie dzięki zastosowaniu zmiennej liczby argumentów

Kod: Zaznacz cały

#define ERR(name, fmt, ...)   fprintf(stderr, "ERROR " #name ": " fmt "\n", \
                                  __VA_ARGS__)


Użycie...

Kod: Zaznacz cały

ERR(errCantOpen, "File %s cannot be opened", filename);


Więcej na temat poprawnego zdefiniowania tego narzędzia tu:

https://en.wikipedia.org/wiki/Variadic_macro

Dzięki VLA, można łatwiej obsługiwać struktury pakietów (C99)

Kod: Zaznacz cały

void process(uint32_t payload_size) {
    uint8_t buffer[sizeof(Protocol_t) + payload_size];
    ....
}


Efekt podobny do alloca() ale nieco lepiej opisuje intencje.

Wyczyszczenie bufora wejściowego

Kod: Zaznacz cały

scanf("%*[^\n]%*c");


Lambda w GCC

Dzięki temu że w GCC można definiować funkcje wewnątrz innych funkcji, z użyciem małego makra można zdefiniować lambdy:

Kod: Zaznacz cały

#define lambda(return_type, function_body) \
    ({ return_type fn function_body fn })


Użycie:

Kod: Zaznacz cały

lambda (int, (int x, int y) { return x > y; })(1, 2)


Rozwija się do:

Kod: Zaznacz cały

({ int fn (int x, int y) { return x > y } fn; })(1, 2)


Funkcja printf() umożliwia formatowanie zależne od zmiennej

Kod: Zaznacz cały

#include <stdio.h>
#include <math.h>
int main(void) {
    int prec;
    printf("Do ilu wartości po przecinku wyświetlić PI?: ");
    scanf("%d", &prec);
    printf("%.*f\n", prec, M_PI);
}


Znacznik w ciągu formatującym to gwiazdka.

Przekazywanie funkcji jako argument

Kod: Zaznacz cały

#include <stdio.h>
typedef void (*function_t)(int);
void fun1(int a) {
    printf("Jestem fun1(%d).\n", a);
}
void fun2(int a) {
    printf("Jestem fun2(%d).\n", a);
}
void executor1(function_t function, int a) {
    (*function)(a);
}
function_t fun_chooser(int i) {
    if(i == 0) {
        return fun1;
    }
    return fun2;
}
int main(void) {
    executor1(fun1, 42);
    fun_chooser(1)(88);
}


Wynik:

Kod: Zaznacz cały

Jestem fun1(42).
Jestem fun2(88).


Funkcje ze zmienną ilością argumentów (tzw. wariadyczne)

http://rosettacode.org/wiki/Variadic_function#C

X-Makro

https://en.wikibooks.org/wiki/C_Programming/Preprocessor#X-Macros

[url]Wewnętrzne makra kompilatora[/url]

Nagłówek intrin.h lub intrinsics.h, zawiera wewnętrzne funkcje i makra kompilatora.
GCC: https://gcc.gnu.org/onlinedocs/gcc/Target-Builtins.html#Target-Builtins
Clang: http://clang.llvm.org/docs/LanguageExtensions.html
Np. dla AVR będzie to <avr/buildins.h>

Oznaczenie warunku jako często występującego lub nie

Kod: Zaznacz cały

Technika dla gcc na platformie x86.

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)
void foo(int arg)
{
     if (unlikely(arg == 0)) {
           do_this();
           return;
     }
     do_that();
     ...
}


Używana w jądrze GNU/Linux.

Blokowanie ostrzeżenia o nieużywanej zmiennej

http://stackoverflow.com/questions/4030959/will-a-variablename-c-statement-be-a-no-op-at-all-times/4030983#4030983

Switch/case z użyciem operatora ?

Kod: Zaznacz cały

string result =
    a==0 ? "zero" :
    a==1 ? "one" :
    a==2 ? "two" :
    0;


Wygenerowanie listy makr wbudowanych

Kod: Zaznacz cały

touch emoty.c
gcc -E -dM empty.c | sort >gcc-macros.txt
clang -E -dM empty.c | sort > clang-macros.txt


https://msdn.microsoft.com/en-us/library/b0084kay(v=vs.140).aspx
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Wróć do „Pisanie programów w C”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 2 gości