Ćwiczenia 10: przydatne triki programistyczne języka C-system, atexit, czas, polskie znaki, itd…

Czas na pokazanie Państwu rzeczy ciekawych, przydatnych, ale nie mających żadnej kategorii. Część z nich będzie działała wyłącznie pod system Linux, ale nie wszystkie.

Komenda system

Jak wyczyścić ekran? No możemy w programie wyświetlić 100 razy nową linię, a jak ekran ma 110? No możemy 1000 razy, a co jeśli ekran ma 10 linii? Operacje wejścia-wyjścia są bardzo wolne. Powiem krótko-w oparciu o standard języka C, bez funkcji danego systemu, czy funkcji biblioteki do manipulowania tekstem takiej jak ncurses.h na systemie Linux nie da się tego zrobić. Ale po co wywarzać otwarte drzwi -na systemie Linux jest komenda:

clear

na systemie Windows komenda

cls

więc użyjmy zewnętrznych komend w programie.

#include <stdio.h>
#include <stdlib.h>

void czyscEkran(void)
{
    fflush(stdout);
    #ifndef __WIN32__
        system("clear");
    #else
        system("cls");
    #endif
}

int main(void)
{
    printf("Czesc!");
    czyscEkran();
    printf("Na razie, dziekuje za wspolprace:)\n");
}

W powyższym kodzie użyłem preprocesora żeby wybadać na jakim systemie operacyjnym pracujemy -jeśli nie __WIN32__ (tak, 64bitowe również mają takie makro) to użyj linuksowego clear. Funkcja printf zajmuje się wyświetlaniem tekstu, robi to w sposób buforowany, czyli nie od razu wyświetla to co się do niej poda, dlatego musiałem wymusić wyczyszczenie bufora standardowego wyjścia:
fflush(stdout);.
Niestety powyższa komenda nie umożliwia nam zwrotu tego co zostało wyświetlone przez wywołany program. Jeśli zależy nam na zwróceniu outputu powinniśmy użyć innych rzeczy, takich jak np. potoki (o tym kiedy indziej). Ewentualnie możemy przekierować wydruk do pliku i odczytać z pliku.

Czas

Fajnie jest wiedzieć coś na temat czasu w języku C. Funkcje do zabawy z czasem znajdują się w:

#include <time.h>

Przydatną funkcją jest funkcja time, która zwraca liczbę sekund od 1 stycznia 1970 roku.

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t czasTeraz = time(NULL);
    const unsigned SEKUND_NA_NIEPRZESTEPNY_ROK = 60*60*24*365;
	printf("Od 1 stycznia 1970 minelo sekund %ld\n", czasTeraz);
	printf("Czyli okolo %ld lat\n", czasTeraz/SEKUND_NA_NIEPRZESTEPNY_ROK);
}

wydruk:

Od 1 stycznia 1970 minelo sekund 1419872474
Czyli okolo 45 lat

Gdy chcemy operować na konkretnej dacie
Fajnie że jest funkcja time(), ale nie jest ona wygodna jeśli chcemy operować na konkretnym czasie.
Np. chcemy dokładnie wiedzieć który dzisiaj i która godzina:

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t czasTeraz = time(NULL);;
    struct tm* strukturaCzasu;

    strukturaCzasu = localtime ( &czasTeraz );

    printf("Dzisiaj mamy: rok %d, miesiac %d, dzien %d, czas %d:%d\n",
           strukturaCzasu->tm_year + 1900,
           strukturaCzasu->tm_mon  + 1,
           strukturaCzasu->tm_mday,
           strukturaCzasu->tm_hour,
           strukturaCzasu->tm_min);
}

O strukturach powiemy kiedy indziej. Natomiast dodałem do roku, ponieważ tm->tm_year zawiera liczbę lat od 1900 roku, a miesiące zaczynają od 0. Struktura tm zawiera również inne pola http://www.cplusplus.com/reference/ctime/tm/
Funkcja localtime konwertuje time_t na struct tm.

Łatwiejsze formatowaie czasu
Na czasie operować można łatwiej niz w ostatnim przykładzie -mamy do tego funkcje: asctime() i ctime(), które na swój sposób wyświetlą nam obecny czas.

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t czasTeraz = time(NULL);
    struct tm* strukturaCzasu;

    strukturaCzasu = localtime(&czasTeraz);

    printf("Obecny czas (funkcja asctime()): %s\n", asctime(strukturaCzasu));
    printf("Obecny czas (funkcja ctime()): %s\n",   ctime(&czasTeraz));
}

Jak Państwo widzą obydwie funkcje zwracają to samo, ale z różnych parametrów, funkcji ctime() wystarczy to co zwróci funkcja time().

Jeśli ktoś chce mieć swój format czasu, to też jest do tego funkcja strftime() http://www.cplusplus.com/reference/ctime/strftime/

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t czasTeraz = time(NULL);
    struct tm* strukturaCzasu = localtime(&czasTeraz);
	char bufor[80];

	strftime(bufor, sizeof(bufor),"Czas wg mojego formatu %d.%m(%B).%Y (%A), %H:%S", strukturaCzasu);
	puts(bufor);
}

Liczenie czasu, oraz czekanie
Żeby program poczekał parę sekund mamy wiele sposobów. Przenośnym jest operowanie na tickach procesora

#include <stdio.h>
#include <time.h>

void czekaj(int sekundy)
{
	clock_t pozadanaIloscTykniec = clock() + (sekundy * CLOCKS_PER_SEC);
    while(clock() < pozadanaIloscTykniec);
}

int main(void)
{
    time_t czasOd, czasDo;

    czasOd = time(NULL);
    czekaj(4);
    czasDo = time(NULL);

	printf("Operacja zajela %lf sekund\n", difftime(czasDo, czasOd));
}

Niestety takie czekanie mimo iż czeka powoduje ciągłe wykonywanie obliczeń => zużywa nam jeden rdzeń procesora.
Dlatego lepiej użyć funkcji zależnych od platformy:

#include <stdio.h>
#include <time.h>

#ifdef __linux
#include <unistd.h>
#elif defined(__WIN32__)
#include <windows.h>
#endif

void czekaj(unsigned sekundy)
{
	#ifdef __WIN32__
		Sleep(1000*sekundy);
	#elif __linux
		sleep(sekundy);
	#else
		clock_t pozadanaIloscTykniec = clock() + (sekundy * CLOCKS_PER_SEC);
		while(clock() < pozadanaIloscTykniec);
	#endif
}

int main(void)
{
    time_t czasOd, czasDo;

    czasOd = time(NULL);
    czekaj(4);
    czasDo = time(NULL);

    printf("Operacja zajela %lf sekund\n", difftime(czasDo, czasOd));
}

Na różnych systemach jest możliwe jeszcze dokładniejsze liczenie czasu, np. na systemie Linux w milisekundach
https://www.cs.rutgers.edu/~pxk/416/notes/c-tutorials/gettime.html .
Jeśli ktoś jest ciekaw ile czasu potrzebuje jego program do działania, ale nie potrzebuje tego za każdym razem polecam skorzystać z systemowej funkcji time z poziomu konsoli:

time ./program.exe

Assercje

Czyli przerwanie wykonywania programu w sytuacji jeśli pewien warunek nie jest spełniony. Możemy to zrobić delikatnie:

if(warunek)
{
    fprintf(stderr, "Wystapil blad\n");
    exit(-1);
}

A możemy to zrobić bardziej brutalnie, jeśli błąd jest naprawdę poważny:

#include <assert.h>

/* ... */
assert(warunek);

Takie brutalne wyłączenie programu wywołuje funkcje abort();, która to natychmiastowo wyłącza program, nie są dokonywanie czyszczenia buforów ani usuwane zmienne.

Assercje są świetną rzeczą, jeśli są dokonywane w trakcie kompilacji:

#if __STDC_VERSION__ < 199901L
#eror "Za stary kompilator jezyka C, wymagany co nie mniej niz kompatybilny z C90!"
#endif

Na szczeście wraz z nadejściem standardu języka C z roku 2011 pojawiła się statyczna assercja, która umożliwia zatrzymanie kompilacji programu w momencie gdy warunek nie jest spełniony:

void validacja(void)
{
	_Static_assert(sizeof(int) == 4, "typ int musi miec 4 bajty");
	_Static_assert(sizeof(short) >= 2, "typ short musi miec >= 2 bajty");
	_Static_assert(sizeof(long) >= 4, "typ long musi miec >= 8 bajty");
}

Funkcje do pracy z tekstem

Bez takich funkcji w standardzie byłoby kiepsko. Oto przykłady funkcji.

strcat() -dopisywanie tekstu na koniec napisu (koniec to pierwsze wystąpienie znaku końca linii ). Funkcja może służyć do łączenia napisów:

	char imie[] = "Jan";
	char nazwisko[] = "Kowalski";
	char imie_nazwisko[100];

	strcat(imie_nazwisko, imie);
	strcat(imie_nazwisko, " ");
	strcat(imie_nazwisko, nazwisko);
	printf("imie_nazwisko = %s\n", imie_nazwisko);

sprintf() -służy do wpisywania treści zmiennych o określonym formacie do bufora:

	char imie[] = "Jan";
	char nazwisko[] = "Kowalski";
	char bufor[100];

	sprintf(bufor, "%s %s, ma lat %d", imie, nazwisko, 44);
	puts(bufor);

Wydruk:

Jan Kowalski, ma lat 44

Oczywiście dodam, że zbieżność osób i nazwisk jest przypadkowa:D.

Kopiowanie i porównywanie napisów
Do tego celu służą funkcje strcpy() do kopiowania i strcmp() do porównywania, jeśli napisy są takie same zostanie zwrócona wartość 0.

	char tekst[] = "Ala ma kota";
	char bufor[100];

	strcpy(bufor, tekst); /* kopiuje tekst na bufor */
	puts(bufor);

	if(strcmp(tekst, bufor) == 0) /* porownuje 2 napisy */
		puts("Napisy sa takie same");

	printf("Napis '%s' ma dlugosc %d\n", tekst, strlen(tekst));

Wyszukiwanie
Zacznijmy od wyszukiwania jednej litery, funkcja strchr() lub strpbrk():

	char tekst[] = "Ala ma kota";

	const char* wystapienieListeryK = strchr(tekst, 'k');
	printf("litera k wystepuje na pozycji %d, od tej pozycji jest napis '%s'\n",
	       wystapienieListeryK - tekst, wystapienieListeryK);

wydruk:

litera k wystepuje na pozycji 7, od tej pozycji jest napis 'kota'

Teraz wyszukiwanie całego ciągu znaków, funkcja strstr():

        char tekst[] = "Ala ma kota";
	char szukanyTekst[] ="kot";
	char* wskaznikDoZnalezionejPozycji;
	wskaznikDoZnalezionejPozycji = strstr(tekst, szukanyTekst);
	if(wskaznikDoZnalezionejPozycji)
		printf("znaleziono tekst '%s' na pozycji %d\n", szukanyTekst,
		wskaznikDoZnalezionejPozycji-tekst);

i demonstracja wydruku:

znaleziono tekst 'kot' na pozycji 7

Sprawdzanie ile pierwszych znaków należy do zbioru dozwolonych znaków:

        char napis[] = "nazywam sie XXX YYY. Mam Lat 23 ...";
	char zbiorDozwolonychZnakow[] = "abcdefghijklmnopstwuyz";

	unsigned pozycja = strspn(napis, zbiorDozwolonychZnakow);

	puts(napis);
	printf ("Dozwolonych znakow jest %d, pierwszy niedozwolony znak '%c'\n",
	        pozycja,
	        napis[pozycja]);

wydruk:

nazywam sie XXX YYY. Mam Lat 23 ...
Dozwolonych znakow jest 7, pierwszy niedozwolony znak ' '

Dzielenie ciagu znaków na tokeny, funkcja strtok() http://www.cplusplus.com/reference/cstring/strtok/ .

Z ciekawych funkcji tekstowych mamy jeszcze konwersję znaków dużych na małe i na odwrót:

#include <ctype.h>

int tolower(int c);
int toupper(int c);

Przydatne są też wszelakie funkcje do klasyfikacji tekstu:

#include <ctype.h>

int isalnum(int c); /* sprawdza czy znak jest litera lub cyfra */
int isalpha(int c); /* sprawdza czy znak jest litera */
int isdigit(int c); /* sprawdza czy znak jest liczba */
int isspace(int c); /* sprawdza czy znak jest bialym znakiem */

więcej klasyfikacji w dokumentacji: http://www.cplusplus.com/reference/cctype/


Parsowanie argumentów programu

Często chcemy sterować naszym programem przez podane argumenty uruchomienia programu. Użytkownik może podać różne argumenty, a my chcemy obsługiwać konkretne. Możemy albo sami napisać parsowanie argumentów albo skorzystać z gotowych rozwiązań.
Na Linuxie mamy dostępną funkcję getopt()

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>

int main(int argc, char* argv[])
{
    double liczba = 0;
    int argument = 0;
    char napis[100];
    int help = 0;
    memset(napis, 0, sizeof(napis));

    int znak;
    const char* obslugiwaneArgumenty = "a:l:n:hv";

    while ( (znak = getopt(argc, argv, obslugiwaneArgumenty)) != -1) {
        int this_option_optind = optind ? optind : 1;
        switch (znak)
        {
			case 'a':
				argument = atoi(optarg);
				printf ("podano argument %d\n", argument);
				break;
			case 'l':
				liczba = atof(optarg);
				printf ("podano liczbe %lf\n", liczba);
				break;
			case 'n':
				strncpy(napis, optarg, sizeof(napis)-1);
				printf ("podano napis '%s'\n", napis);
				break;
			case 'h':
				printf ("wlaczono tryb help\n");
				help = 1;
				break;
        }
    }
    if(optind < argc)
    {
        printf("nieobslugiwane agrumenty: ");
        while(optind < argc)
            printf("%d) %s ", optind, argv[optind++]);
        printf ("\n");
    }

    if(help)
    {
		printf("Obslugiwane argumenty: '%s'\n", obslugiwaneArgumenty);
		return 0;
	}

	printf("Podano: argument=%d\tliczba=%lf\tnapis=%s\n",
	       argument, liczba, napis);
	return 0;
}

uruchomienie przykładowe:

$ gcc tmp.c && ./a.out -a 3 co_to_jest? -l 44 -n 'Ala bez kota'
podano argument 3
podano liczbe 44.000000
podano napis 'Ala bez kota'
nieobslugiwane agrumenty: 8) co_to_jest?
Podano: argument=3	liczba=44.000000	napis=Ala bez kota

Jak Państwo widzą w funkcji getopt o argumentach decyduje argument „obslugiwaneArgumenty”. Jeśli podamy samą literę x to spodziewa się argumentu -x, jeśli podamy literę z dwukropkiem spodziewa się argumentu, który jest ustawiony w zmiennej optarg. Mamy również dostępny indeks przeparsowanych argumentów optind.

Oprócz funkcji getopt mamy również dostępną funkcj getopt_long. Umożliwia ona obsługę argumentów zaróno w formie krótkiej:
-a, jak i długiej -argument. Więcej informacji na temat getopt_long: https://www.gnu.org/software/libc/manual/html_node/Getopt-Long-Options.html


Obsługa błędów

Wiele funkcji w sytuacji błędu zwraca kod błędu. Niektóre z nich poza kodem błędu zmieniają wartość specjalnej zmiennej o nazwie:

extern int errno;

ta zmienna jest zdefiniowana w:

#include <errno.h>

Oto przykład obsługi błędów -pierwiastek z ujemnej liczby:

#include <stdio.h>
#include <math.h>
#include <errno.h>

int main(void)
{
    double zmienna = -2.2;
    double pierwiastek = sqrt(zmienna);

    if(EDOM == errno)
		perror("Blad");
	else
		printf("Pierwiastek z %lf = %lf\n", zmienna, pierwiastek);

	return 0;
}

Wydruk:

Blad: Numerical argument out of domain

Niestety w standardzie zmienna errno jest bardzo uboga:
http://www.cplusplus.com/reference/cerrno/errno/
natomiast różne kompilatory mają wiele dodatkowych kodów błędów dla zmiennej errno, oto lista dla kompilatora gcc:
https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html

Na szczęście nie musimy sprawdzać wystąpienia każdego błędu w celu wyświetlenia komunikatu z informacją o błędzie, mamy do tego odpowiednie funkcje perror, która wyświetli dla nas treść błędu. Mamy również funkcję strerror(), która zwróci nam napis odzwierciedlający obecny błąd.


Lokalki

Kultury się różnią, anglojęzyczni mają wszystko inne niż reszta krajów, ale poza anglikami występują też pewne różnice w różnych kulturach. Np. znak przecinka w liczbach, znak waluty itp. W języku C jest możliwość ustawienia „lokalek”, czyli odpowiednich zachowań kulturowych.

Oto jak zwykle przykład:

#include <stdio.h>
#include <locale.h>

int main(void)
{
  setlocale(LC_ALL, ""); /* ustawienie aktualnych lokalek */
  struct lconv* naszaLokalka = localeconv();

  printf("Symbol lokalnej waluty: %s\n", naszaLokalka->currency_symbol);
  printf("Miedzynarodowy symbol waluty: %s\n", naszaLokalka->int_curr_symbol);
  printf("PI = %lf\n", 3.1415926);
  return 0;
}

Oto wydruk -polskie znaki i przecinek zamiast kropki:

Symbol lokalnej waluty: zł
Miedzynarodowy symbol waluty: PLN
PI = 3,141593

Niestety ustawienie to jest bardzo ubogie, ale jednak jest, więcej na ten temat dokumentacji:
http://www.cplusplus.com/reference/clocale/

Polskie znaki

Jest to problem rodzący się u każdego programisty. Pytanie czy naprawdę jest nam to potrzebne? Jeśli tak zdecydujemy polecam poniższe linki, wydaje się, że dotyczą C++, ale uzasadnienie jest ponadjęzykowe:
http://cpp0x.pl/artykuly/?id=55
http://miroslawzelent.pl/kurs-c-plus-plus-polskie-znaki-konsola-terminal-windows-linux-macos/


Ustawianie akcji podczas wyłączania programu

Sporadycznie zdarza się, że chcemy tuż przed wyjściem z programu dokonać pewnych akcji sprzątających. Da się to zrobić, wystarczy każdą taką akcję zarejestrować funkcją atexit():

#include <stdio.h>
#include <stdlib.h>

void funkcja1(void)
{
  printf("Koncze program, wykonuje sie %s\n", __FUNCTION__);
}

void funkcja2(void)
{
  printf("Koncze program, wykonuje sie %s\n", __FUNCTION__);
}

int main ()
{
  atexit(funkcja1);
  atexit(funkcja2);
  puts ("Ostatnie wypisanie");
  return 0;
}

wydruk:

Ostatnie wypisanie
Koncze program, wykonuje sie funkcja2
Koncze program, wykonuje sie funkcja1

Ważne -funkcje zostają wywoływane w kolejności odwrotnej do ich rejestrowania funkcją atexit().

I jeszcze pytanie kiedy akcje z atexit zadziałają:

  • jeśli zamkniemy program normalnie, czyli wraz z dotarciem do końca funkcji main,
  • jeśli wyjdziemy z programu przy pomocy funkcji exit().

Ale funkcje nie zostaną wywołane, jeśli:

  • wyjdziemy z programu brutalniej, przy użyciu:
    quick_exit(), abort(), terminate(),
  • w przypadku otrzymania i nieprzechwycenia sygnału naruszenia ochrony pamięci, lub w przypadku zabicia programu przez system operacyjny.

Testy jednostkowe

Tutaj tylko napomnę, gdyż testowanie to bardzo szeroki temat. Jeśli programujemy dla kogoś i planujemy rozwijać nasz produkt może okazać się, że koniecznym będzie dodatkowy narzut programistyczny -konieczność napisania testów!
Testy jednostkowe to automatyczne testy pojedyńczych funkcjonalności programu na wiele różnych sposobów.
Często przyjmuje się, że napisanie kodu to 1/3, natomiast przetestowanie napisanego kodu zajmują 2/3 czasu.
Testy mają swoich zwolenników i przeciwników, moja opinia jest taka -należy zachować zdrowy rozsądek.
Dzięki testom możemy sobie dopisywać funkcjonalności i wiemy czy coś zepsuliśmy -brak obaw przed zmianami kodu.
Testowanie na piechotę nie jest wygodne, dlatego powstały specjalne narzędzia służące do ułatwienia pisania testów, przykłady takich narzędzi:
http://stackoverflow.com/questions/65820/unit-testing-c-code
https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#C

Pomysłem na projekt zaliczeniowy jest prościutki programik (wystarczy zestaw jakiś funkcji) + testy do tego przy użyciu któregoś z powyższych narzędzi.


Rozszerzenia jezyka C

Na zajęciach uczymy się standardu języka C, czyli jak pisać programy w języku C i nie przejmować się, że nasz program po przeniesieniu na inny system przestanie działać. Niestety często jak coś piszemy to sprawdzamy czy coś działa, a nie czy to co napisaliśmy jest zgodne ze standardem, z tego powodu może się nam zdażyć napisanie nieprzenośnego kodu. Jeśli będziemy coś pisali dla jakiegoś klienta, który będzie wymagał przenośności między systemami, warto abyśmy zapoznali się z rozszerzeniami języka C dostępnymi pod kompilatorem, pod którym piszemy. Gcc ma sporo rozszerzeń:
https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html

Bardzo ciekawym rozszerzeniem jest wielowątkowy język C: UPC, bardzo ułatwiający pisanie programów wielowątkowych, dla dociekliwych:

http://upc.lbl.gov/

Ćwiczenia 9: przydatne narzędzia programisty -makefile, scons, kdiff, gnuplot, …

Makefile

Jest to bardzo popularny program do automatyzacji procesu budowania plików.
Na potrzeby zajęć będziemy go stosowali do kompilacji i konsolidacji (=linkowania) wielu plików.

Po co nam automatyzacja budowy
W ramach ćwiczeń pisaliśmy jedno-plikowe programy, niemniej jednak w życiu nie pisze się niemalże nigdy programów jedno-plikowych w językach kompilowanych takich jak C. Powodów na to jest bardzo wiele, m.in.:

  • kompilacja większych projektów trwa, dlatego dąży się do przebudowywania tylko tego co konieczne
  • mamy możliwość jednorazowego użycia ustawień: ścieżek do includowania, define’ów, flag kompilacji i linkowania, optymalizacji itp…
    zbudowanie projektu wielo-plikowego byłoby bardzo nieprzyjemną rzeczą (budowanie każdego pliku z osobna). Oczywiście można zbudować wszystkie pliki z danym rozszerzeniem znajdujące się pod daną ścieżką, ale tylko pod daną ścieżką:

    gcc *.c # zbuduje wszystkie pliki z rozszerzeniem .c w danym katalogu
  • ustawiamy reguły budowania jeden raz i jak wrócimy do projektu po roku nie musimy się zastanawiać jak go budowaliśmy.

Jak używać makefile
Mamy sobie plik plik.c, najprostszy makefile może wyglądać tak:

plik.exe:
    gcc plik.c -o plik.exe

run: plik.exe
    ./plik.exe

Ważne tutaj są: dwukropek za nazwą wynikowego pliku/reguły, oraz znak tab (nie spacje) przed każdą nową regułą.
I teraz co powyższy makefile robi: aby utworzyć plik plik.exe zostaje wykonana komenda gcc… . Aby wykonać komendę run musi istnieć plik plik.exe, jeśli nie istnieje zostaje wykonana komenda nazwana plik.exe, po jej pomyślnym wykonaniu dopiero komenda run.

Programu używamy w taki sposób, że tworzymy sobie plik o nazwie „makefile”, oraz uruchamiamy:
make komenda

Czas na kolejny przykład makefile-a, mamy pliki: main.c, wypisywanie.h, wypisywanie,c, wczytywanie.h, wczytywanie.c. Pliki te znajdują się we wpisie na temat funkcji, ale zawartości plików nie znaczenia, byleby wewnątrz pliku main.c była funkcja main(), poza tym plik main.c włącza wypisywanie.h i wczytywanie.h. Plik wczytywanie.c włącza wczytywanie.h.
Plik wypisywanie.c włącza wypisywanie.h. Poza tymi plikami tworzymy plik o nazwie „makefile”. Dla tych zależności plik makefile może wyglądać tak:

main.o: main.c wczytywanie.h wypisywanie.h makefile
	gcc main.c -c -o main.o

wczytywanie.o: wczytywanie.c wczytywanie.h makefile
	gcc wczytywanie.c -c -o wczytywanie.o

wypisywanie.o: wypisywanie.c wypisywanie.h makefile
	gcc wypisywanie.c -c -o wypisywanie.o

program.exe: main.o wczytywanie.o wypisywanie.o
	gcc  main.o wczytywanie.o wypisywanie.o -o program.exe

run: program.exe
	./program.exe

Jeśli użytkownik zmieni coś w pliku main.c to tylko ten plik zostanie ponownie przekompilowany. Podobne działanie jest z plikami wczytywanie.c i wypisywanie.c. Zmiana w plikach .h pociągnie za sobą konieczność przekompilowania większej liczby plików, ale przy dużych projektach zysk jest o wiele większy dzięki zastosowaniu automatyzacji budowy.

Zmienne w makefile
Powyższy przykład był fajny, ale dużą zaletą makefile’a jest możliwość operowania na zmiennych. Mamy też pewne zmienne specjalne:

  • $< – zastępuje tą zmienną pierwszą komendą z listy zależności (pierwszy wyraz za dwukropkiem)
  • $@ – zastępuje tą zmienną właśnie wykonywaną komendą (to przed dwukropkiem)
  • $^ – zastępuje tą zmienną wszystkimi zależnościami (wszystkim za dwukropkiem)

Mamy też możliwość ukrycia wykonywanej komendy poprzez zastosowanie @ przed komendą, poza tym funkcjonuje komenda echo, która umożliwia wyświetlanie. Mamy też znak #, po którym jest komentarz. Oto makefile z zastosowaniem tego wszystkiego + zmiennych:

cc = gcc
flags = -Wall -pedantic -Wextra
linker_flags = -lm
include_path = -I.
defines = -DDEBUG
library_path = -L.
libs = 

compiler_flags = $(flags) $(linker_flags) $(include_path) $(defines)
linker_flags += $(library_path)
linker_flags += $(libs)

###### reguly budowania:
main.o: main.c wczytywanie.h wypisywanie.h makefile
	@$(cc) $(compiler_flags) &lt;pre wp-pre-tag-3=""&gt;&lt;/pre&gt;amp;amp;lt; -c -o $@
	@echo 'CC' $@

wczytywanie.o: wczytywanie.c wczytywanie.h makefile
	@$(cc) $(compiler_flags) &lt;pre wp-pre-tag-3=""&gt;&lt;/pre&gt;amp;amp;lt; -c -o $@
	@echo 'CC' $@

wypisywanie.o: wypisywanie.c wypisywanie.h makefile
	@$(cc) $(compiler_flags) &lt;pre wp-pre-tag-3=""&gt;&lt;/pre&gt;amp;amp;lt; -c -o $@
	@echo 'CC' $@

program.exe: main.o wczytywanie.o wypisywanie.o
	@$(cc) $(compiler_flags) $^ -o $@ $(linker_flags)
	@echo 'EXE' $@

run: program.exe
	./$^

Pozwolę sobie wyjaśnić zastosowanie użytych zmiennych:

cc = gcc # domyslny kompilator
flags = -Wall -pedantic -Wextra # flagi dla kompilatora, uzylem wiekszej ilosci ostrzezen
linker_flags = -lm # flagi potrzebne podczas, czyli gdy podczas kompilacji nie jest znana definicja np. funkcji
include_path = -I. # dodatkowe sciezki do includowania, nie wszystko jest w domyslnych lokalizacjach systemowych
defines = -DDEBUG # sa to definicje preprocesora uzywane podczas kompilacji
library_path = -L. # jak korzystamy z jakiejs biblioteki w ten sposob podajemy sciezki gdzie sie znajduja poszczegolne rzeczy
libs = # tutaj podajemy konkretne biblioteki ktorych chcemy uzyc

Dla lepszego wyjaśnienia czas na przykład. Ściągamy sobie bibliotekę LibHaru http://cpp0x.pl/artykuly/?id=54 do programowalnego generowania plików PDF. Skompilujemy bibliotekę i nie mamy dostępu, lub nie chcemy dodać tej biblioteki do systemowych lokalizacji. Wtedy w naszym projekcie tworzymy sobie 2 katalogi: include, do którego przenosimy pliki z nagłówkami i libs, do którego przenosimy skompilowane pliki biblioteki.
Wtedy zmienne z naszego makefile’a będą wyglądać np. tak:

linker_flags = -lm
include_path = -Iinclude
defines = -DDEBUG
library_path = -Llibs
libs = -lhpdf -lpng -lstdc++

Dodatkowe możliwości makefile’a
Program makefile może uruchamiać dowolne komendy, nie tylko budowanie:

clean:
	@rm -rf *~
	@rm -rf *.o

tar:
	(tar -czvf zadania.tar.gz cw4_makefile.c wczytywanie.h wczytywanie.c wypisywanie.h wypisywanie.c makefile)

Aby nie pisać zawsze: make komenda możemy pisać tylko make i zostanie wykonana domyślna komenda. W tym celu jako pierwszą regułę piszemy:

all: komenda

Możemy też raz napisać komendę jak budować pewne pliki, a potem pisać tylko zależności:

%.o: %.c %.h makefile
	@$(cc) $(compiler_flags) &lt;pre wp-pre-tag-6=""&gt;&lt;/pre&gt;amp;amp;lt; -c -o $@
	@echo 'CC' $@

wczytywanie.o: wczytywanie.c wczytywanie.h makefile
wypisywanie.o: wypisywanie.c wypisywanie.h makefile

WAŻNE: Zależność musi być dokładnie taka sama jak reguła, czyli dla
%.o: %.c %.h makefile
zadziałają:
wczytywanie.o: wczytywanie.c wczytywanie.h makefile
ale już nie zadziałają:
main.o: main.c wczytywanie.h wypisywanie.h makefile

Ciekawą rzeczą jest możliwość wywoływania zewnętrznych komend przez makefile:

gitStatus = $(shell git status)

To jeszcze nie koniec, makefile ma możliwość wywoływania innych makefile’ów, ale w taki sposób, że jeśli podczas wykonywania wewnętrznych makefile’ów pojawi się błąd to dalsze wykonywanie reguł jest wstrzymywane.

build_subcomponent:
	@$(MAKE) -C subcomponent_directory --no-print-directory

To jeszcze nie koniec możliwości! Mając parę plików możemy dodać do nich suffix i prefix:

pliki = plik1 plik2 plik3
pliki_w_sciezce := $(addprefix $(sciezkaDoPlikow)/, $(pliki))
pliki_skompilowane := $(addsuffix .o/, $(pliki_w_sciezce))

Automatyczne generowanie dependencji do makefile
Jeśli używamy makefile do kompilacji programu zależy nam żeby przebudowywało się tylko to co potrzeba. Jeśli zależności będą skomplikowane to robi się problem, na szczęście gcc ma możliwość generowania dependencji do makefile’a.
A makefile może włączać tak wygenerowane pliki, przykładowe tutoriale jak to zrobić:
http://stackoverflow.com/questions/1981563/generate-all-project-dependencies-in-a-single-file-using-gcc-mm-flag
http://scottmcpeak.com/autodepend/autodepend.html

Co jeszcze daje makefile
Makefile ma też mnóstwo innych możliwości, więcej na ich temat:
https://www.gnu.org/software/make/manual/make.html
Można też poczytać w języku polskim wprowadzenia do makefile’i: 1, 2


Scons

Chwilę temu pokazałem bardzo popularne narzędzie o nazwie makefile. Jak Państwo widzieli musieliśmy się bardzo dużo napisać i dłuuuugo szukać ewentualnych błędów. Potrzeba matką wynalazków, dlatego zirytowani makefilem użytkownicy napisali o wiele prostrze w użyciu narzędzie -scons.
Używamy tego tworząc plik o nazwie: SConstruct. Przykładowy dla naszego projektu może wyglądać tak:

zbudowano = Program(['main.c', 'wypisywanie.c', 'wczytywanie.c'], CCFLAGS='-Wall -pedantic -Wextra')

print 'Zbudowano: ', zbudowano

Sposoby uruchomienia:

~/jezykC_zajecia/cw9_dodatki$ scons -Q
Zbudowano:  ['main']
gcc -o main.o -c -Wall -pedantic -Wextra main.c
gcc -o wypisywanie.o -c -Wall -pedantic -Wextra wypisywanie.c
gcc -o wczytywanie.o -c -Wall -pedantic -Wextra wczytywanie.c
gcc -o main main.o wypisywanie.o wczytywanie.o

Mamy również wbudowaną możliwość czyszczenia:

~/jezykC_zajecia/cw9_dodatki$ scons -c
scons: Reading SConscript files ...
Zbudowano:  ['main']
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed main.o
Removed wypisywanie.o
Removed wczytywanie.o
Removed cw4_makefile
scons: done cleaning targets.

A dodatkowe o dodatkowych możliwościach programu mogą Państwo przeczytać tutaj:
http://www.scons.org/doc/HTML/scons-man.html


Rozwiązywanie błędów niezasygnalizowanych przez kompilator

Jako ludzie jesteśmy omylni. Nawet mając wielkie doświadczenie programistyczne szansa popełnienia błędu nigdy nie spada do 0. O dużej liczbie potencjalnych błędów powie nam kompilator, szczególnie jak podamy mu pare flag o których często wspominam:
-Wall -pedantic -Wextra
Niestety kompilator mówi nam jedynie o błędach składniowych, a nie o błędach logicznych.


Debugger

Program się kompiluje, bez żadnych ostrzeżeń, ale efekt działania programu jest dla nas niezadowalający.
W tej sytuacji możemy co kawałek wyprintfować informacje o kluczowych zmiennych, ale jest to pracochłonne, nieeleganckie, musimy potem te printfy wyrzucać. Pracując w różnych firmach zetknąłem się z przypadkami jak ktoś zapomniał usunąć jakiegoś printfa i wielu miało okazje zobaczyć jakieś polskie brzydkie słówko w repozytorium na którym pracuje kilka/kilkadziesiąt/kilkuset pracowników.

Przykład użycia debuggera
Mamy sobie kompilujący się kod:

#include 

int main(int argc, char *argv)
{
    int i, liczba, silnia;
    if(argc=1)
        liczba = atoi(argv[1]);

    for (i=1; i		&amp;amp;amp;lt;liczba; i++)
        silnia=silnia*i;

    printf("%d! =  %d\n", liczba, silnia);

    return 0;
}

gdy skompilujemy go w najszybszy sposób i uruchomimy zobaczymy:

gcc cw1_debug.c &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out
Naruszenie ochrony pamięci (core dumped)

Zacznijmy od dodania paru flag podczas kompilacji:

gcc cw1_debug.c -Wall -pedantic -Wextra &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out
cw1_debug.c:4:5: warning: second argument of ‘main’ should be ‘char **’ [-Wmain]
cw1_debug.c: In function ‘main’:
cw1_debug.c:6:5: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
cw1_debug.c:7:9: warning: implicit declaration of function ‘atoi’ [-Wimplicit-function-declaration]

Faktycznie -już widać parę błędów, poprawmy je (poprawny drugi argument maina, przypisanie w IFie zamiast porównania, włączenie stdlib.h, który zawiera atoi):

#include
#include 

int main(int argc, char **argv)
{
    int i, liczba, silnia;
    if(argc == 1)
        liczba = atoi(argv[1]);

    for (i=1; i		&amp;amp;amp;lt;liczba; ++i)
        silnia=silnia*i;

    printf("%d! =  %d\n", liczba, silnia);

    return 0;
}

kod kompiluje się już bez podpowiedzi dla nas:

gcc cw1_debug.c -Wall -pedantic -Wextra &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out
Naruszenie ochrony pamięci (core dumped)

więc czas na użycie debuggera. Wpierw musimy powiedzieć kompilatorowi, żeby do skompilowanego programu dołączył pewne informacje umożliwiające śledzenie kodu, w kompilatorze gcc służy do tego flaga -g:

gcc cw1_debug.c -g

następnie uruchamiamy nasz program przez debugger:

gdb a.out

pojawi się nam trochę tekstu, a „znaki zachęty” w nowej linii wygląda tak:

(gdb)

najpierw uruchommy program (komenda run):

(gdb) run
Starting program: /home/grzegorz/jezykC_zajecia/cw9_dodatki/a.out 

Program received signal SIGSEGV, Segmentation fault.
0xb7e471b0 in ?? () from /lib/i386-linux-gnu/libc.so.6
(gdb)

wiele to nam nie mówi, więc użyjmy „najważniejszej” (wg mnie) komendy debbugera – backtrace

(gdb) backtrace
#0  0xb7e471b0 in ?? () from /lib/i386-linux-gnu/libc.so.6
#1  0xb7e46f67 in strtol () from /lib/i386-linux-gnu/libc.so.6
#2  0xb7e4426f in atoi () from /lib/i386-linux-gnu/libc.so.6
#3  0x08048433 in main ()

widzimy stos wywołań funkcji. Wywołujemy od funkcji main, potem atoi (zamiana napisu na liczbę), potem widzimy że pod spodem są wołane inne, dziwne rzeczy (strtol to …..). Ale problem najprawdopodobniej jest w niewłaściwym wywołaniu funkcji którą jawnie wywołujemy, tj. atoi(). Jest to funkcja, która z łańcucha znakowego pozyzkuję liczbę typu int. Właśnie, „z łańcucha znakowego”, a co dostaje? Patrzymy na podejrzany kod:

    if(argc == 1)
        liczba = atoi(argv[1]);

no i widać -przecież program ma zawsze co najmniej jeden argument -nazwę programu. Więc powyższy warunek jest błędny, trzeba go zmienić żeby sprawdzał czy podano co najmniej jeden dodatkowy argument.
Z debuggera wychodzimy wpisując q, ewentualnie quit.

#include
#include 

int main(int argc, char **argv)
{
    int i, liczba, silnia;
    if(argc &amp;amp;amp;gt; 1)
        liczba = atoi(argv[1]);
    else
    {
        printf("!!! Podano za malo argumentow, uzycie: %s liczba\n", argv[0]);
        return -1;
    }

    for (i=1; i		&amp;amp;amp;lt;liczba; ++i)
        silnia=silnia*i;

    printf("%d! =  %d\n", liczba, silnia);

    return 0;
}

teraz uruchamiając w niewłaściwy sposób zobaczymy:

$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out
!!! Podano za malo argumentow, uzycie: ./a.out liczba
$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out 2
2! =  134513833

Program się nie wywala, ale dalej coś jest nie tak, więc trzeba przejść od nowa przez program:

  • break -tworzy breakpoint
  • next -idzie do następnej instrukcji bez wchodzenia do funkcji
  • step -kolejna instrukcja
  • print -wyświetla zmienną
  • continue -kontynuuje wykonywanie od czasu ostatniego breakpointu
~/jezykC_zajecia/cw9_dodatki$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; gdb a.out
(gdb) break main
Breakpoint 1 at 0x804841d: file cw1_debug.c, line 7.
(gdb) run
Starting program: /home/grzegorz/jezykC_zajecia/cw9_dodatki/a.out

Breakpoint 1, main (argc=1, argv=0xbffff144) at cw1_debug.c:7
7           if(argc &amp;amp;amp;gt; 1)
(gdb) print argc
$1 = 1
(gdb) continue
Continuing.
!!! Podano za malo argumentow, uzycie: /home/grzegorz/jezykC_zajecia/cw9_dodatki/a.out liczba
[Inferior 1 (process 19424) exited with code 0377]
(gdb) run 2
Starting program: /home/grzegorz/jezykC_zajecia/cw9_dodatki/a.out 2

Breakpoint 1, main (argc=2, argv=0xbffff144) at cw1_debug.c:7
7           if(argc &amp;amp;amp;gt; 1)
(gdb) print argc
$2 = 2
(gdb) next
8               liczba = atoi(argv[1]);
(gdb)
15          for (i=1; i		&amp;amp;amp;lt;liczba; ++i)
(gdb) print liczba
$3 = 2
(gdb) step
16              silnia=silnia*i;
(gdb) print silnia
$4 = 134513833
(gdb) continue

i już Państwo widzą co było przyczyną -niezainicjowana zmienna silnia. Dokonajmy więc poprawek i:

#include
#include 

int main(int argc, char **argv)
{
    int i, liczba, silnia = 1;
    if(argc &amp;amp;amp;gt; 1)
        liczba = atoi(argv[1]);
    else
    {
        printf("!!! Podano za malo argumentow, uzycie: %s liczba\n", argv[0]);
        return -1;
    }

    for (i=1; i		&amp;amp;amp;lt;liczba; ++i)
        silnia=silnia*i;

    printf("%d! =  %d\n", liczba, silnia);

    return 0;
}

Uruchamiamy i:

~/jezykC_zajecia/cw9_dodatki$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out 3
3! =  2
~/jezykC_zajecia/cw9_dodatki$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out 2
2! =  1

dalej coś jest nie tak, więc sprawdźmy co:

~/jezykC_zajecia/cw9_dodatki$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; gdb a.out
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
...
Reading symbols from /home/grzegorz/jezykC_zajecia/cw9_dodatki/a.out...done.
(gdb) break 15
Breakpoint 1 at 0x804843f: file cw1_debug.c, line 15.
(gdb) run 2
Starting program: /home/grzegorz/jezykC_zajecia/cw9_dodatki/a.out 2

Breakpoint 1, main (argc=2, argv=0xbffff144) at cw1_debug.c:15
15          for (i=1; i		&amp;amp;amp;lt;liczba; ++i)
(gdb) step
16              silnia=silnia*i;
(gdb) print silnia
$1 = 1
(gdb) print i
$2 = 1
(gdb) step
15          for (i=1; i&amp;amp;amp;lt;liczba; ++i)
(gdb) step
18          printf("%d! =  %d\n", liczba, silnia);
(gdb) print silnia
$3 = 1
(gdb) c
Continuing.
2! =  1
[Inferior 1 (process 22561) exited normally]
(gdb) q

no właśnie -pętla dla wartości 2 powinna się wykonać z i==2, a nie wykonała, dokonajmy poprawek:

#include
#include 

int main(int argc, char **argv)
{
    int i, liczba, silnia = 1;
    if(argc &amp;amp;amp;gt; 1)
        liczba = atoi(argv[1]);
    else
    {
        printf("!!! Podano za malo argumentow, uzycie: %s liczba\n", argv[0]);
        return -1;
    }

    for (i=1; i&amp;amp;amp;lt;=liczba; ++i)
        silnia=silnia*i;

    printf("%d! =  %d\n", liczba, silnia);

    return 0;
}

i wydruk jest taki jak trzeba:

~/jezykC_zajecia/cw9_dodatki$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out 2
2! =  2
~/jezykC_zajecia/cw9_dodatki$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out 3
3! =  6
~/jezykC_zajecia/cw9_dodatki$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out 4
4! =  24
~/jezykC_zajecia/cw9_dodatki$ gcc cw1_debug.c -Wall -pedantic -Wextra -g &amp;amp;amp;amp;&amp;amp;amp;amp; ./a.out 10
10! =  3628800

Tekstu mają Państwo bardzo wiele, ale zrozumiawszy powyższe (i przećwiczywszy samodzielnie) odkryją Państwo zaletę debuggera.

Komendy debuggera
W powyższym przykładzie użyłem paru komend debuggera gdb:

  • run [argumenty] -uruchamia program
  • break [plik:]funkcja|numer_linii -zatrzymuje się w danej linii
  • step -wykonuje następną linię
  • next -jw. ale bez wchodzenia do wnętrza funkcji
  • continue -dalsze wykonywanie programu
  • print zmienna – wyświetla wartość zmiennej
  • quit -wyjście z debuggera gdb
  • list linia_od, linia_do – wyświetla kod
  • help komenda – wyświetla informacje o komendzie
  • file program.exe -ładuje program

dodatkowo w temacie komend:

  • w komendzie break można napisać np:
    break linia if argc > 1
  • kolejnym ułatwieniem jest że nie trzeba pisać zawsze pełnych komend, można operować:
    b, p, c, q, bt, …
  • świetną rzeczą jest to że nie trzeba zawsze pisać tej samej komendy, enter ponawia ostatnią komendę.
  • możemy również napisać np.
    step 10 -przejdzie nam o 10 instrukcji do przodu
    next 10

więcej komend znajdą Państwo:
http://www.yolinux.com/TUTORIALS/GDB-Commands.html

Cofanie w GDB

Od pewnej wersji debuggera zostało to wprowadzone, jednakże trzeba to włączyć po uruchomienia programu (po wpisaniu run) przez komendę: target record-full. Lista dostępnych komend do cofania.
Oczywiście ta opcja nie jest domyślnie włączona nie przez zaniedbanie, tylko dlatego, że nagrywanie debuggerem wymaga dużo dodatkowej pamięci.

Cgdb
Gdb ma wiele nakładek, jedną z nich jest cgdb, który to wyświetla kod w kolorach podczas debuggowania.

Kolejny przykład programu z błędem
Dla przećwiczenia debbugera polecam kolejny program do przedebuggowania:
https://en.wikipedia.org/wiki/GNU_Debugger

VS debugger
Wg wielu programistów najlepszym debbugerem jest debbuger Microsoftu znajdujący się wewnątrz środowiska programistycznego Visual Studio.

Valgring
Jest to narzędzie dynamicznej analizy kodu, służy do wykrywania niewłaściwego operowania na pamięci, tj. niezwolnionej pamięci, pisania w niedozwolonych miejscach w pamięci itp.:
http://valgrind.org/


Inne przydatne narzędzia dla programistów

Diff
Narzędzie do porównywania dwóch dokumentów -różnice są w formie tekstowej.

Kdiff
Narzędzie do porównywania dokumentów, ale też całych katalogów, w formie graficznej, bardzo wygodne i intuicyjne, polecam!

Gnuplot
Jest to program do rysowania, mający naprawdę bardzo wiele możliwości.
Uruchamiamy program pisząc:

gnuplot

aby wyświetlić funkcję np. sin piszemy:

plot sin(x)

Możemy również wyświetlić z pliku:

plot 'plik.txt'

Jeśli chce ktoś lepiej poznać gnuplota polecam: http://cpp0x.pl/artykuly/?id=52


Optymalizacja w trakcie kompilacji

W językach C i C++, jak i wielu innych bardzo ważnym aspektem jest optymalność. Dobrze jak wiemy co jest optymalne, a co nie, natomiast oprócz tego kompilator dokonuje za nas optymalizacji. Możemy chcieć od kompilatora wygenerowania szybszego kodu, kosztem wydłużenia czasu kompilacji. Na kompilatorze gcc służą do tego flagi kompilatora:
-O1, -O2, … -O6
np:

gcc program.c -O3

Oczywiście to tylko sugestia dla kompilatora, nie ma gwarancji, że skompilowany program będzie się wykonywał szybciej.


Czytelność kodu
Ciągle męczę Państwa o czytelność kodu i ciągle i ciągle. Rezultat tego jest różny. Ale nie robię tego bo mi się tak podoba, tylko dlatego, że w życiu programisty czytelność jest bardzo ważna, więc warto ją umieć zanim się pójdzie do pracy.
Co rozumiemy przez czytelność -wszystko. Ale istnieją pewne wytyczne (są o tym całe książki). W podstronie Warunki Zaliczenia znajduje się parę wytycznych, których wymagam na projekcie zaliczeniowym.
Nie ma wytycznych wszędzie najlepszych -każda firma (szanująca się i rozwijająca duży produkt) ma pewne wytyczne na temat kodu, których pracownicy muszą się trzymać. Jest to rozsądne bo dzięki temu cały kod jest w podobnym stylu napisany.
Przykłady Coding Standardów:
http://stackoverflow.com/questions/1262459/coding-standards-for-pure-c-not-c
https://www.doc.ic.ac.uk/lab/cplus/cstyle.html
http://www.maultech.com/chrislott/resources/cstyle/
i wiele innych przykładów.

Duckduckgo
Największą skarbnicą wiedzy dla informatyka jest wyszukiwarka internetowa! Na wiele problemów możemy otrzymać odpowiedź bez konieczności pytania kogokolwiek. Nie tylko wątpliwości możemy rozwiewać przy jej pomocy, możemy również się uczyć dzięki jej pomocy. Dla informatyków umiejętność korzystania z wyszukiwarki jest nieodzowna -wszak jak nie my to kto ma umieć z tego korzystać? Niestety informatyka jest zdominowana przez języka angielski, dlatego dobrze jest nauczyć się w tym właśnie języku nauczyć wyszukiwania interesującyc nas haseł. Jak nie wiemy jak jest coś po angielsku (jakieś specjalistyczne określenie) to najczęściej Wikipedia wie.
Polecam nie najpopularniejszą przeglądarkę, tylko tą którą cenię najbardziej –duckduckgo.com. Mimo pozorów wg mnie radzi sobie niegorzej od tej na g.

Ćwiczenia 7: wskaźniki część 1

Wskaźniki -czym są, z czym się je je

Wskaźnik jest to zmienna zawierająca informacje o adresie w pamięci. Wskaźnikami wskazujemy na zmienną znajdującą się pod adresem na który wskazuje wskaźnik.

Na wskaźnikach operujemy w następujący sposób

typ* zmienna_wsk; // definicja wskaznika
typ liczba = ...;

zmienna_wsk = &liczba;  // przypisanie do wskaznika adresu zmiennej
zmienna_wsk = 24563242; // przypisanie do wskaznika adresu
                        // bedacego wartoscia 24563242

*zmienna_wsk // odniesienie do komorki pamieci na ktora wskazuje
             // dany wskaznik (wyluskanie). Mozna zarowno odczytywac
             // i przypisywac do wyluskanego wskaznika

*zmienna_wsk = 44;     // przypisanie wartosci 44 do pamieci wskazanej 
                       // wskaznikiem zmienna_wsk

typ* zmienna_wsk = &liczba; // inicjowanie wskaznika adresem zmiennej
                            // liczba od razu w momencie definiowania wskaznika
typ* zmienna_wsk2;
zmienna_wsk2 = zmienna_wsk; // przypisanie adresu zmiennej_wsk do
                            // zmienna_wsk2. Zarowno zmiennej_wsk
                            // jak i zmienna_wsk2 to wskazniki

Na początek polecam zapoznać się z opisem i przykładami kolegi, który zezwolił mi na korzystanie ze swoich materiałów:
http://jezykc.wordpress.com/2014/11/07/cwiczenia-05/
W powyższym linku zostały omówione następujące kwestie:

  • Co to są wskaźniki
  • Jak się na nich operuje
  • Konwertowalność tablic do wskaźników w języku C
  • Wskaźniki void* i konwertowalność wszystkich wskaźników na ten typ wskaźnika
  • Wskaźniki stałe, wskaźniki na stałe, stałe wskaźniki na stałe.
  • Zmiana wartości argumentów przekazywanych do funkcji -przez wskaźniki
  • Rzutowanie wskaźników

w ramach uzupełnienia dodam przykład na manipulacje wskaźnikami:

#include 

int main()
{
    int a,b,c, *ptr;

    ptr = &a;
    *ptr = 2;
    ptr = &b;
    *ptr = 4;
    ptr = &c;
    *ptr = 7;

    printf("a=%d, b=%d, c=%d.  ptr=%p (%d)\n", a, b, c, ptr, ptr);

	return 0;
}

Zmiana wskaźnika do funkcji

Funkcje w C przyjmując argumenty przez kopię, jeśli chcemy zmienić to na co wskazuje wskaźnik musimy przekazać do funkcji wskaźnik do wskaźnika.
Oto przykład na kopiowania napisu:

#include 
#include 
#include  /* strlen() */
#include  /* toupper() */

void skopiujNapisZmieniajacLiteryNaDuze(const char* zrodlowyNapis,
                                        char** tuZapiszKopieNapisu)
{
    unsigned dlugoscNapisu = strlen(zrodlowyNapis) + 1;
    *tuZapiszKopieNapisu = (char*)malloc(sizeof(char)*dlugoscNapisu);
    /* o funkcji malloc powiem kiedy indziej */
    int i;

    for(i=0; i<dlugoscNapisu; ++i)
        (*tuZapiszKopieNapisu)[i] = toupper( zrodlowyNapis[i] );
}

int main()
{
    const char* napis = "Widzialem orla cien";
    char* kopiaNapisuDuzymi;

    skopiujNapisZmieniajacLiteryNaDuze(napis, &kopiaNapisuDuzymi);
    printf("Napis = '%s', kopia = '%s'\n", napis, kopiaNapisuDuzymi);

    return 0;
}

Wskaźniki warto nazywać w odpowiedni sposób, spotkałem się z kończeniem nazwy zmiennej wskaźnikowej suffixem Ptr (z ang. pointer). Np.: int* numberPtr;

Operacje arytmetyczne na wskaźnikach

Wskaźnik zawiera w sobie adres w pamięci, adres to liczba, dlatego można wykonywać na adresach operacje arytmetyczne, przykład:

#include 
#include 

int main()
{
    int tab[] = { 1,2,3,4,5 };
    int *wsk = tab;

    wsk++;
    ++wsk;
    wsk += 3;
    wsk -= 4;

    printf("*wsk=%d, jest to pozycja o indeksie %d\n", *wsk, wsk-tab);
    return 0;
}

Można też wykonywać inkrementację i dekrementację wskaźników, niemniej jednak nie polecam tego robić w nieczytelny sposób.

#include 

int main()
{
    int tab[] = { 2, 4, 5, 1 };
    int *wsk = tab;

    printf("*wsk=%d\n", *wsk++);
    printf("*wsk=%d\n", *wsk++);
    printf("*wsk=%d\n", (*wsk)++);
    printf("*wsk=%d\n", ++*wsk);

    return 0;
}

Mając dwa wskaźniki i stosując operacje arytmetycze można uzyskać rozmiar tablicy:

#include 
#include 
#include  /* strlen() */
#include   /* toupper() */

void wypiszZakres(int *wskOd, int* wskDo)
{
    printf("Zakres do wypisania ma dlugosc %d\n", wskDo-wskOd);
    for(; wskOd!=wskDo; ++wskOd)
        printf("%d, ", *wskOd);
    puts("");
}

int main()
{
    int tablica[] = {1,2,3,4,5,6,7,8,9};

    wypiszZakres(tablica, &tablica[9]);

    return 0;
}

Rozmiar tablicy znakowej i rozmiar napisu- Ważne!

Zacznę od rozmiaru wskaźników:

#include 

int main()
{
    int*            int_wsk;
    double*         double_wsk;
    unsigned*       unsigned_wsk;
    long long*      long_long_wsk;
    unsigned char*  unsigned_char_wsk;
    const unsigned* const_unsigned_wsk;

    printf("sizeof(%s) = %d\n", "int*", sizeof(int_wsk));
    printf("sizeof(%s) = %d\n", "double*", sizeof(double_wsk));
    printf("sizeof(%s) = %d\n", "unsigned*", sizeof(unsigned_wsk));
    printf("sizeof(%s) = %d\n", "long long*", sizeof(long_long_wsk));
    printf("sizeof(%s) = %d\n", "unsigned char*", sizeof(unsigned_char_wsk));
    printf("sizeof(%s) = %d\n", "const unsigned*", sizeof(const_unsigned_wsk));

    return 0;
}

wydruk:

sizeof(int*) = 4
sizeof(double*) = 4
sizeof(unsigned*) = 4
sizeof(long long*) = 4
sizeof(unsigned char*) = 4
sizeof(const unsigned*) = 4

WNIOSEK: wskaźniki zajmują tyle samo miejsca w pamięci!
Oczywiście na różnych systemach może to być inna wartość.
Dla dociekliwych: w rozszerzeniu języka C o nazwie UPC http://upc.lbl.gov/ który ma wskaźniki współdzielone między wątkami, te wskaźniki zajmują więcej w pamięci.

Ważny przykład -długość napisu i sizeof napisu

#include 
#include  /* strlen() */

int main()
{
    const char* napis1 = "Ala nie ma juz kota";
    char* napis2 = " ma psa";
    char napis3[] = ", oraz szczura";

    printf("'%s%s%s'\n", napis1, napis2, napis3);
    printf("sizeof('%s')=%d, strlen('%s')=%d\n", napis1, sizeof(napis1), napis1, strlen(napis1));
    printf("sizeof('%s')=%d, strlen('%s')=%d\n", napis2, sizeof(napis2), napis2, strlen(napis2));
    printf("sizeof('%s')=%d, strlen('%s')=%d\n", napis3, sizeof(napis3), napis3, strlen(napis3));
    return 0;
}

a oto wydruk:

'Ala nie ma juz kota ma psa, oraz szczura'
sizeof('Ala nie ma juz kota')=4, strlen('Ala nie ma juz kota')=19
sizeof(' ma psa')=4, strlen(' ma psa')=7
sizeof(', oraz szczura')=15, strlen(', oraz szczura')=14

I teraz opis:
Jak napiszemy napis od ręki w kodzie … = „ala juz nie ma kota!”; to jest on typu const char* i tak też powinien być taktowany, jednakże kiedyś taki napis był traktowany jako char* i dla kompatybilności zostało w ten sposób. W języku C można w ten sposób zainicjować tablicę znaków char tablicaZnakow[] = „hej tam pod lasem…”;. Dygresja -w języku C++ tylko pierwszy z tych sposobów nie powoduje żadnych ostrzeżeń podczas kompilacji.

Teraz kwestia różnicy w długości i rozmiarze -jak wiać we wcześniejszym przykładzie każdy wskaźnik zawiera ten sam rozmiar, więc tu nie ma problemu. Istotna różnica jest jeśli chodzi o tablicę znaków (w naszym przykłdzie napis3).
Sizeof zwraca informacje ile zajmuje dany typ, lub tablica. Strlen zwraca informację ile znaków jest w napisie do czasu napotkania znaku końca wiersza ”, lub po prostu wartości 0. Dlatego sizeof będzie zawsze większy o co najmniej jeden.

Operowanie na wskaźnikach do stałych i zwykłych wskaźnikach

Mamy zmienne i stałe, również to dotyczy wskaźników:

#include 
#include 

int main()
{
    const int liczba = 2;
    const int *cptr = &liczba; /* OK: wskaznik do stalej wskazuje na stala */
    int* ptr = &liczba; /* warning: uzywamy niestalego wskaznika do stalej */
    int* ptr2 = (int*)&liczba; /* OK: rzutujemy */
    int* ptr3 = cptr; /* warning: uzywamy wskaznika aby wskazal na to samo co wstkaznik do stalej */
    int* ptr4 = (int*)cptr; /* OK: rzutujemy */

    return 0;
}

Oczywiście wydruk powyższego programu może być inny dla języka C i języka C++, gdyż ten drugi bardziej przestrzega stałości stałych.

O co zdarza się że pytają na rozmowach ze wskaźników -dla dociekliwych

Ogólnie bardzo lubią z tego pytać bo wiele osób tego nie umie, pokażę jednak bardzo wredne przykłady:

#include 

int main()
{
    int tablica[2][3] = { {1,2,3}, {-5,-10,-15} };
    int* wsk = (int*)tablica;
    int i;

    for(i=0; i<2*3; ++i)
        printf("wsk[%d]=%d\n", i, wsk[i]);

    return 0;
}

Jaki będzie wydruk?
Tablica to pewna liczba elementów umieszczona w pamięci jeden za drugim, niezależnie od ilości indeksów.

Z innej rozmowy:

#include
#include /* assert() -przerywa program
jesli warunek nie jest spelniony */

int main()
{
unsigned char tablica[] = {0,1,2,3,4,5,6,7,8,9,10,11,12};
unsigned char* wsk_uchar = (unsigned char*)tablica;
unsigned short* wsk_ushort = (unsigned short*)tablica;
unsigned int* wsk_uint = (unsigned int*)tablica;

assert(sizeof(unsigned char) == 1);
assert(sizeof(unsigned short) == 2);
assert(sizeof(unsigned int) == 4);

printf(„*wsk_uchar=%u, *wsk_ushort=%u, *wsk_uint=%u\n”, *wsk_uchar, *wsk_ushort, *wsk_uint);

++wsk_uchar;
++wsk_ushort;
++wsk_uint;

printf(„*wsk_uchar=%u, *wsk_ushort=%u, *wsk_uint=%u\n”, *wsk_uchar, *wsk_ushort, *wsk_uint);

return 0;
}

Jak się do tego wziąć? char ma 1 bajt, short ma 2,
czyli wartości wyglądają tak:
unsigned char = liczba w bitach = liczba szesnastkowo:
0 = 0000 0000 = 0 0
1 = 0000 0001 = 0 1
2 = 0000 0010 = 0 2
3 = 0000 0011 = 0 3
4 = 0000 0100 = 0 4
jeśli chcemy odczytać wskaźnikiem do typu short odczytamy 2 bajty (=16 bitów), czyli idąc od początku tablicy:
0000 0000 0000 0001, to da nam 1, typ int też da nam jeden. A jeśli przesuniemy wskaźnik? Przesuwamy o tyle żeby wskazywał na następną pozycję dla danego typu, więc po inkrementacji wskaźnik short wskazuje na:
0000 0010 0000 0011, co daje szesnastkowo 0 2 0 3, czyli dziesiętnie 515 typ int da:
0000 0100 0000 0101 0000 0110 0000 0111, szesnastkowo 4050607, czyli dziesiętnie 67438087.
Dla utrudnienia trzeba uwzględnić endianowość https://pl.wikipedia.org/wiki/Kolejno%C5%9B%C4%87_bajt%C3%B3w

Wskaźnik typu restrict -dla dociekliwych

W standardzie języka C z roku 1999 wprowadzono pewną optymalizację -wskaźniki restrict, oznaczają one że na daną pamięć wskazuje się tylko tym wskaźnikiem, przez co kompilator może dokonać pewnych optymalizacji, więcej informacji:
http://stackoverflow.com/questions/745870/realistic-usage-of-the-c99-restrict-keyword

Dlaczego warto stosować wskaźniki?

  • Żeby nie być zagiętym podczas rozmowy kwalifikacyjnej:D.
  • aby funkcja mogła zmieniać wartości zmiennych
  • aby uniknąć wielokrotnego kopiowania
  • aby móc operować na tablicach statycznych (ich rozmiar jest znany podczas kompilacji) i dynamicznych (o tym następnym razem)
  • i inne zastosowania

 

Zadania

  1. Funkcja, która przyjmuje 2 wskaźniki i ustawia na podaną wartość to co między nimi (polecam żeby to była tablica). Przy okazji funkcja podaje ile elementów zainicjowała.
  2. Funkcja zwracająca wskaźnik do pierwszego elementu nie będącego literą alfabetu.
  3. W skrócie printf ze scanfem w jednym. Czyli funkcję, która przyjmuje napis i nieokreśloną liczbę zmiennych.
    Funkcja wyszukuje wystąpienia %d (uprośćmy do tego jednego typu); wszystko co nie jest %d ma zostać wypisane, natomiast w momencie napotkania każdego %d musi czekać na liczbę użytkownika. Dla uproszczenia można przekazywać tablicę zmiennych, ale lepiej będzie jeśli Państwo przećwiczyli by wielokropek „funkcja(„napis”, …)”.
    Przykładowy napis:
    Podaj krotszy bok prostokata: %d teraz podaj dluzszy: %d
    Podpowiedź -można ustawiać wskaźnik na odpowiednie miejsce, a w miejsce %d dawać znak końca stringa ”, wtedy łatwo można wyświetlić funkcją printf bez użycia pętli.
    A wyszukiwać można przy użyciu strstr().
  4. Utworzyć tablice A 10 liczb i tablice B 10 wskaźników, z której każdy będzie pokazywał na odpowiednią liczbę z tablicy A. Następnie tablica B ma zostać posortowana wg wartości zmiennych z tablicy A na którą wskazują. Następnie ma zostać wyświetlona posortowana tablica A. Rezultat końcowy: tablica A jest niezmieniona a zostaje wyświetlona w kolejności rosnącej.

(Zgodnie z obietnicą: za zrobienie wszystkich zadań z zajęć 7, oraz wszystkich z zajęć 8 dostaje się dodatkowego plusa za zadania)