Przejdź do zawartości

C/Wersja do druku

Z Wikibooks, biblioteki wolnych podręczników.
< C


Programowanie w C




 
 

Spis treści

[edytuj]
  1. Wstęp
    1. O podręczniku
    2. O języku C
    3. Czego potrzebujesz
    4. Używanie kompilatora
  2. C dla początkujących
    1. Pierwszy program
    2. Podstawowe wiadomości
    3. Zmienne w C
    4. Operatory
    5. Instrukcje sterujące
    6. Podstawowe procedury wejścia i wyjścia
    7. Funkcje
    8. Preprocesor
    9. Biblioteka standardowa
    10. Czytanie i pisanie do plików
    11. Ćwiczenia
  3. Zaawansowany C
    1. Tablice
    2. Wskaźniki
    3. Napisy
    4. Typy złożone
    5. Tworzenie bibliotek
    6. Więcej o kompilowaniu
    7. Zaawansowane operacje matematyczne
    8. Powszechne praktyki
    9. Przenośność programów
    10. Łączenie z innymi językami
    11. Ćwiczenia
  4. Dodatek A
    1. Składnia
    2. Przykłady z komentarzem
  5. Przypisy

O podręczniku

[edytuj]

O czym jest ten podręcznik?

[edytuj]

Niniejszy podręcznik stanowi przewodnik dla początkujących programistów w języku programowania C.

Jaka minimalna wiedza jest potrzebna?

[edytuj]

Ten podręcznik ma nauczyć programowania w C od podstaw do poziomu zaawansowanego. Do zrozumienia rozdziału dla początkujących wymagana jest jedynie znajomość podstawowych pojęć z zakresu algebry oraz terminów komputerowych. Doświadczenie w programowaniu w innych językach bardzo pomaga, ale nie jest konieczne.

Czy mogę pomóc?

[edytuj]

Oczywiście, że możesz. Mało tego, będziemy zadowoleni z każdej pomocy – możesz pisać rozdziały lub tłumaczyć je z angielskiej wersji tego podręcznika. Nie musisz pytać się nikogo o zgodę - jeśli chcesz, możesz zacząć już teraz. Prosimy jedynie o zapoznanie się ze stylem podręcznika, użytymi w nim szablonami i zachowanie układu rozdziałów. Propozycje zmiany spisu treści należy zgłaszać na stronie dyskusji. Konwencje użyte w podręczniku opisane są w module Konwencje.

Jeśli znalazłeś jakiś błąd, a nie umiesz go poprawić, koniecznie powiadom o tym fakcie autorów tego podręcznika za pomocą strony dyskusji danego modułu książki. Dzięki temu przyczyniasz się do rozwoju tego podręcznika.

Przyjęte konwencje

[edytuj]

Informacje ważne oznaczamy w następujący sposób:

Dodatkowe informacje, które odrobinę wykraczają poza zakres podręcznika, a także wyjaśniają kwestie niezwiązane bezpośrednio z językiem C, oznaczamy tak:

Ponadto kod w języku C będzie prezentowany w następujący sposób:

#include <stdio.h>

int main (int argc, char *argv[])
{
  return 0;
}

Innego rodzaju przykłady, dialog użytkownika z konsolą i programem, wejście / wyjście programu, informacje teoretyczne będą wyglądały tak:

typ zmienna = wartość;

Więcej o konwencjach przyjętych w kodzie w module Konwencje

Autorzy

[edytuj]

Istotny wkład w powstanie podręcznika mają:

Dodatkowo w rozwoju podręcznika pomagali między innymi:


Źródła

[edytuj]


O języku C

[edytuj]

C jest językiem programowania wysokiego poziomu. Jego nazwę interpretuje się jako następną literę po B (nazwa jego poprzednika), lub drugą literę języka BCPL (poprzednik języka B).


Cechy

[edytuj]
  • proceduralny[1]
  • imperatywny
  • ogólnego przeznaczenia
  • wystandaryzowany

Historia C

[edytuj]
Dennis Ritchie (right), the inventor of the C programming language, with Ken Thompson

W 1947 roku trzej naukowcy z Bell Telephone Laboratories - William Shockley, Walter Brattain i John Bardeen - stworzyli pierwszy tranzystor; w 1956 roku, w MIT skonstruowano pierwszy komputer oparty wyłącznie na tranzystorach: TX-O; w 1958 roku Jack Kilby z Texas Instruments skonstruował układ scalony. Ale zanim powstał pierwszy układ scalony, pierwszy język wysokiego poziomu został już napisany.

W 1954 powstał Fortran (Formula Translator), który zapoczątkował napisanie języka Fortran I (1956). Później powstały kolejno:

  • Algol 58 - Algorithmic Language w 1958 r.
  • Algol 60 (1960)
  • CPL - Combined Programming Language (1963)
  • BCPL - Basic CPL (1967)
  • B (1969)

i C w oparciu o B.

B został stworzony przez Kena Thompsona z Bell Labs; był to język interpretowany, używany we wczesnych, wewnętrznych wersjach systemu operacyjnego UNIX. Inni pracownicy Bell Labs, Thompson i Dennis Ritchie, rozwinęli B, nazywając go NB; dalszy rozwój NB dał C - język kompilowany. Większa część UNIX-a została ponownie napisana w NB, a następnie w C, co dało w efekcie bardziej przenośny system operacyjny. W 1978 roku wydana została książka pt. "The C Programming Language", która stała się pierwszym podręcznikiem do nauki języka C.

Możliwość uruchamiania UNIX-a na różnych komputerach była główną przyczyną początkowej popularności zarówno UNIX-a, jak i C; zamiast tworzyć nowy system operacyjny, programiści mogli po prostu napisać tylko te części systemu, których wymagał inny sprzęt, oraz napisać kompilator C dla nowego systemu. Odkąd większa część narzędzi systemowych była napisana w C, logiczne było pisanie kolejnych w tym samym języku.

Kilka z obecnie powszechnie stosowanych systemów operacyjnych takich jak Linux, Microsoft Windows zostało napisanych w języku C.

Standaryzacje

[edytuj]
1999 ISO C Concepts

W 1978 roku Ritchie i Kerninghan opublikowali pierwszą książkę nt. języka C - "The C Programming Language". Owa książka przez wiele lat była swoistym "wyznacznikiem", jak programować w języku C. Była więc to niejako pierwsza standaryzacja, nazywana od nazwisk twórców "K&R". Oto nowości, wprowadzone przez nią do języka C w stosunku do jego pierwszych wersji (pochodzących z początku lat 70.):

  • możliwość tworzenia struktur (słowo struct)
  • dłuższe typy danych (modyfikator long)
  • liczby całkowite bez znaku (modyfikator unsigned)
  • zmieniono operator "=+" na "+="

Ponadto producenci kompilatorów (zwłaszcza AT&T) wprowadzali swoje zmiany, nieobjęte standardem:

  • funkcje nie zwracające wartości (void) oraz typ void*
  • funkcje zwracające struktury i unie
  • przypisywanie wartości strukturom
  • wprowadzenie słowa kluczowego const
  • utworzenie biblioteki standardowej
  • wprowadzenie słowa kluczowego enum

Owe nieoficjalne rozszerzenia zagroziły spójności języka, dlatego też powstał standard, regulujący wprowadzone nowinki. Od 1983 roku trwały prace standaryzacyjne, aby w 1989 roku wydać standard C89 (poprawna nazwa to: ANSI X3.159-1989). Niektóre zmiany wprowadzono z języka C++, jednak rewolucję miał dopiero przynieść standard C99, który wprowadził m.in.:

  • funkcje inline
  • nowe typy danych (np. long long int)
  • nowy sposób komentowania, zapożyczony od C++ (//)
  • przechowywanie liczb zmiennoprzecinkowych zostało zaadaptowane do norm IEEE
  • utworzono kilka nowych plików nagłówkowych (stdbool.h, inttypes.h)

standard C99 jest dostępny tutaj.

Rozwój języka C trwał dalej. Na dzień dzisiejszy normą obowiązującą jest norma C11. Wprowadzono w niej m.in. wsparcie programowania równoległego.

Zobacz

Zastosowania języka C

[edytuj]
The C Programming Language

Zalety języka C

[edytuj]

Język C został opracowany jako strukturalny język programowania do celów ogólnych. Przez całą swą historię (czyli ponad 30 lat) służył do tworzenia przeróżnych programów - od systemów operacyjnych po programy nadzorujące pracę urządzeń przemysłowych. C, jako język dużo szybszy od języków interpretowanych (Perl, Python) oraz uruchamianych w maszynach wirtualnych (np. C#, Java) może bez problemu wykonywać złożone operacje nawet wtedy, gdy nałożone są dość duże limity czasu wykonywania pewnych operacji. Jest on przy tym bardzo przenośny - może działać praktycznie na każdej architekturze sprzętowej pod warunkiem opracowania odpowiedniego kompilatora. Często wykorzystywany jest także do oprogramowywania mikrokontrolerów i systemów wbudowanych. Wiele interpreterów języków skryptowych (Perl, PHP, Python. Ruby) jest napisanych w języku C.

Kolejną zaletą C jest jego dostępność - właściwie każdy system typu UNIX posiada kompilator C, w C pisane są funkcje systemowe. Dzięki temu w języku C można pisać aplikacje współpracujące z systemem uniksopodobnym (poprzez interfejs programowy systemu operacyjnego, np. POSIX). Przykładowym zastosowaniem jest programowanie równoległe poprzez funkcje "POSIX threads" albo dynamiczne ładowanie bibliotek (np. interpreter język Perl jest w stanie skompilować fragment kodu do poziomu bilioteki i ją podlinkować dynamicznie). Temat jest opisany w książkach o programowaniu systemu UNIX.

Wady języka C

[edytuj]

Problemem w przypadku C jest zarządzanie pamięcią, które nie wybacza programiście błędów, niewygodne operowanie napisami i niestety pewna liczba "kruczków", które mogą zaskakiwać nowicjuszy. Na tle młodszych języków programowania, C jest językiem dosyć niskiego poziomu więc wiele rzeczy trzeba w nim robić ręcznie, jednak zarazem umożliwia to robienie rzeczy nieprzewidzianych w samym języku (np. implementację liczb 128 bitowych), a także łatwe łączenie C z Asemblerem.

Język C jest dosyć okrojony, tzn. nie posiada wbudowanych bibliotek do typowych struktur danych i algorytmów (w przeciwieństwie do np. C++, Javy). Istnieją co prawda darmowe biblioteki, jednak są one rozwijane oddzielnie.

W przypadku konieczności dużej optymalizacji aplikacji dla danego sprzętu konkurencją staje się język asemblera. W większych projektach programiści sięgają często po obiektowy C++ (np. gry, CAD). W świecie oprogramowania biznesowego wykorzystywane są raczej technologie o szybszym procesie tworzenia aplikacji i mniejszym nacisku na wydajność (np. Java).

Język C w obliczeniach naukowo-technicznych

[edytuj]

Standard C99 zbliżył możliwości języka C do języka Fortran. Dodano niektóre funkcje matematyczne i rozbudowano optymalizację dynamicznie alokowanej pamięci. Standard C11 przyniósł programowanie równoległe. W praktyce wiele zależy od kompilatora. Osiągi narzędzi GNU dla C i Fortranu obu języków bywają zbliżone. Język Fortran posiada jednak np. kotablice oraz obiekty nie występujące w języku C.

Z drugiej strony biblioteka GSL (GNU Scientific Library) jest przykładem projektu, który pomimo upływu czasu nadal wykorzystuje standard ANSI C. Jedną z przyczyn jest dążenie do przenośności. Implementacja liczb zmiennoprzecinkowych w standardzie o podwójnej precyzji została uznana za wystarczającą. W miarę upływu czasu w komputerach są dostępne wyższe precyzje (np. w standardzie C99 jest precyzja rozszerzona (long double)), w procesorach IBM POWER 9 jest możliwa precyzja poczwórna. Tworzenie w pełni uniwersalnego oprogramowania jest w praktyce niemożliwe (bo projekt się zaczyna coraz szybciej rozrastać). "Niech program najpierw w ogóle działa poprawnie, a dopiero potem działa szybko" (złota rada w książce Bruce Eckel-a, "Thinking in C++").

Przyszłość C

[edytuj]

Pomimo sędziwego już wieku (C ma ponad 40 lat) nadal jest on jednym z najczęściej stosowanych języków programowania. Doczekał się już swoich następców, z którymi w niektórych dziedzinach nadal udaje mu się wygrywać. Widać zatem, że pomimo pozornej prostoty i niewielkich możliwości język C nadal spełnia stawiane przed nim wymagania. Warto zatem uczyć się języka C, gdyż nadal jest on wykorzystywany (i nic nie wskazuje na to, by miało się to zmienić), a wiedza którą zdobędziesz ucząc się C na pewno się nie zmarnuje. Składnia języka C, pomimo że przez wielu uważana za nieczytelną, stała się podstawą dla takich języków jak C++, C# czy też Java.

Nowoczesny C





Czego potrzebujesz

[edytuj]

Wbrew powszechnej opinii nauczenie się któregoś z języków programowania (w tym języka C) nie jest takie trudne. Do nauki wystarczą Ci:

Sprzęt

[edytuj]

Język C jest bardzo przenośny, więc będzie działał właściwie na każdej platformie sprzętowej i w każdym nowoczesnym systemie operacyjnym.
Potrzebny będzie komputer z dowolnym systemem operacyjnym, takim jak FreeBSD, Linux, Windows.

Wymagane programy

[edytuj]

Kompilator języka C

[edytuj]
  • Kompilator języka C jest programem który tłumaczy kod źródłowy napisany przez nas do języka asembler, a następnie do postaci zrozumiałej dla komputera (maszyny cyfrowej), czyli do postaci ciągu zer i jedynek które sterują pracą poszczególnych elementów komputera. Kompilator języka C można dostać za darmo.

Przykładem są:

Jako kompilator C może dobrze służyć kompilator języka C++ (różnice między tymi językami przy pisaniu prostych programów są nieistotne). Spokojnie możesz więc użyć na przykład Microsoft Visual C++® lub kompilatorów firmy Borland. Jeśli lubisz eksperymentować, wypróbuj Tiny C Compiler, bardzo szybki kompilator o ciekawych funkcjach. Możesz ponadto wypróbować interpreter języka C. Więcej informacji na Wikipedii.

  • Linker - najczęściej dostarczany jest razem z kompilatorem. Jest to program uruchamiany po etapie kompilacji jednego lub kilku plików źródłowych (pliki z rozszerzeniem *.c, *.cpp lub innym), po procesie kompilacji. Linker łączy wszystkie skompilowane pliki źródłowe i inne funkcje bibliotek (np. printf, scanf) które były użyte (dołączone do naszego programu poprzez użycie dyrektywy #include) w naszym programie, a nie były zdefiniowane w naszych plikach źródłowych. Wywoływany jest on na ogół automatycznie przez kompilator, przez co nie musimy się martwić jego obsługą.

Edytor tekstowy

[edytuj]
Systemy uniksowe oferują wiele edytorów przydatnych dla programisty, jak choćby vim i Emacs w trybie tekstowym, Kate w KDE czy gedit w GNOME. Zwykły notatnik Windowsa wystarcza do pisania prostych programów w C, choć dla wygody można spróbować poszukać w Internecie innych, wygodniejszych narzędzi, takich jak np. Notepad++. Odpowiednikiem Notepad++ w systemie uniksowym jest SciTE. Narzędziami oferującymi więcej niż edytor tekstu są IDE (zintegrowane środowiska programistyczne, opisane niżej).

Dodatkowe narzędzia

[edytuj]
  • strace [2]= narzędzie do analizy kodu badające interakcję programu z jądrem systemu operacyjnego. Śledzi wywołania systemowe oraz sygnały w procesie.


Debuger (opcjonalnie, wg potrzeb)

[edytuj]

Debugger jest to program, który umożliwia prześledzenie (poznanie wartości poszczególnych zmiennych na kolejnych etapach wykonywania programu) linijka po linijce wykonywania naszej aplikacji. Używa się go w celu określenia czemu nasz program nie działa po naszej myśli lub aby zbadać okoliczności występowania błędów. Aby użyć debuggera kompilator musi dołączyć dodatkowe informacje do skompilowanego programu. Przykładowymi debuggerami są: gdb pod Linuksem, lub debugger wchodzący w skład MS Visual Studio.

Zintegrowane środowiska programistyczne

[edytuj]

Zamiast osobnego kompilatora i edytora, możesz wybrać zintegrowane środowisko programistyczne (integrated development environment, IDE). Zawiera ono wszystkie potrzebne narzędzia w jednym: debuger, analizer kodu, podświetlanie składni i - najważniejsze - kompilator oraz linker. Jeśli pisalibyśmy tylko w zwykłym edytorze tekstowym, to:

  1. Utrudnione jest wychwycenie błędów – wszystko wygląda jednostajnie. Pamiętajmy, że nawet drobna literówka składniowa może spowodować, że program po prostu się nie skompiluje. IDE zazwyczaj po nieudanej kompilacji pokazuje gdzie powstał błąd, który poprawiamy i ponownie kompilujemy
  2. Edytor tekstu nie zawiera kompilatora (tak samo notepad++ i inne wzbogacone o podświetlanie składni), więc żeby uruchomić program będziemy musieli zaopatrzyć się w jakieś IDE
  3. Pomimo dużej liczby opcji, IDE nadaje się zarówno dla początkujących jak i zaawansowanych programistów.

Analiza kodu

[edytuj]

Dynamiczna analiza kodu

[edytuj]

Wśród narzędzi które nie są niezbędne, ale zasługują na uwagę, można wymienić Valgrinda[3] – specjalnego rodzaju debugger. Valgrind kontroluje wykonanie programu i wykrywa nieprawidłowe operacje w pamięci oraz wycieki pamięci. Użycie Valgrinda jest proste - kompilujemy program, jak do debugowania, następnie podajemy jako argument Valgrindowi.[4] Istnieje wersja z gui dla Gnome : Alleyoop[5]

Statyczna analiza kodu

[edytuj]

Istnieją również specjalistyczne programy do analizy kodu, np.:[6]

  • Lint (obecnie już nie używane)
  • Splint - rozszerzona wersja Linta[7]
  • Frama C[8]

Formatowanie kodu

[edytuj]

c-reduce

[edytuj]
  • c-reduce to narzędzie redukujące ilość kodu c,c++ lub OpenCl w przypadku raportowania błędów kompilacji

Bibliografia

[edytuj]
  1. quora: Is-C-a-non-procedure-language-or-not
  2. Co skrywa przed Tobą program „Hello World!”? Poznaj jego tajemnice. Dodane przez Robert Bałdyga 29 kwietnia, 2018 w kategorii Linux
  3. Valgrind
  4. Manual jak używać Valgrinda
  5. alleyoop - okienkowa wersja Valgrinda
  6. List of tools for static code analysis
  7. splint - strona domowa
  8. frama-c - is an extensible and collaborative platform dedicated to source-code analysis of C software. It is Open Source software. It works on Windows and Unix (Linux, Mac OS X,…)




Używanie kompilatora

[edytuj]

Język C jest językiem kompilowanym, co oznacza, że potrzebuje specjalnego programu - kompilatora - który tłumaczy kod źródłowy, pisany przez człowieka, na język rozkazów danego komputera. W skrócie działanie kompilatora sprowadza się do czytania tekstowego pliku z kodem programu, raportowania ewentualnych błędów i produkowania pliku wynikowego.

Kompilatory

[edytuj]

Kompilator uruchamiamy:

  • ze zintegrowanego środowiska programistycznego (ang. IDE)
  • z konsoli (linii poleceń)
  • online


Przejść do konsoli można dla systemów:

  • typu UNIX w trybie graficznym użyć programów gnome-terminal, konsole albo xterm,
  • Windows "Wiersz polecenia" (można go znaleźć w menu Akcesoria albo uruchomić wpisując w Start -> Uruchom... "cmd").


Lista kompilatorów

GCC

[edytuj]

GCC (ang. GNU Compiler Collection)[1] jest to darmowy zestaw kompilatorów, m.in. języka C (gcc) rozwijany w ramach projektu GNU. Dostępny jest on na dużą ilość platform sprzętowych, obsługiwanych przez takie systemy operacyjne jak: AIX, *BSD, Linux, Mac OS X, SunOS, Windows. Na niektórych systemach (np. Windows) nie jest on jednak dostępny automatycznie. Należy zainstalować odpowiednie narzędzia (poprzedni rozdział).

Zmienne środowiska

[edytuj]

W bashu:

 export CC="gcc" CFLAGS="-O3 -Wall"


W csh:

setenv CC "gcc"


Możesz dodać to polecenie do pliku ~/.bashrc, aby ustawić to ustawienie na stałe.

Wersja kompilatora

[edytuj]

gcc --version

Otrzymujemy (przykładowy wynik):

gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


lub w c :[2] [3]

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
  printf("GCC_VERSION = %d.%d.%d \n", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
  return 0;
}

Przykładowy wynik :

 GCC_VERSION = 4.8.4

Scieżki

[edytuj]

Z pomocą komendy:[4]

echo "//" | gcc -xc++ -E -v -

przykładowy wynik :

 
Using built-in specs.
COLLECT_GCC=gcc
Target: x86_64-linux-gnu
Configured with: ../src/configure 
-v 
--with-pkgversion='Ubuntu 4.8.4-2ubuntu1~14.04' 
--with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs 
--enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ 
--prefix=/usr 
--program-suffix=-4.8 
--enable-shared 
--enable-linker-build-id 
--libexecdir=/usr/lib 
--without-included-gettext 
--enable-threads=posix 
--with-gxx-include-dir=/usr/include/c++/4.8 
--libdir=/usr/lib 
--enable-nls 
--with-sysroot=/ 
--enable-clocale=gnu 
--enable-libstdcxx-debug 
--enable-libstdcxx-time=yes 
--enable-gnu-unique-object 
--disable-libmudflap 
--enable-plugin 
--with-system-zlib 
--disable-browser-plugin 
--enable-java-awt=gtk 
--enable-gtk-cairo 
--with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64/jre 
--enable-java-home 
--with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64 
--with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.8-amd64 
--with-arch-directory=amd64 
--with-ecj-jar=/usr/share/java/eclipse-ecj.jar 
--enable-objc-gc 
--enable-multiarch 
--disable-werror 
--with-arch-32=i686 
--with-abi=m64 
--with-multilib-list=m32,m64,mx32 
--with-tune=generic 
--enable-checking=release 
--build=x86_64-linux-gnu 
--host=x86_64-linux-gnu 
--target=x86_64-linux-gnu
Thread model: posix
gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04) 
COLLECT_GCC_OPTIONS='-E' '-v' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/4.8/cc1plus 
 -E 
 -quiet 
 -v 
 -imultiarch x86_64-linux-gnu 
 -D_GNU_SOURCE - 
 -mtune=generic 
 -march=x86-64 
 -fstack-protector 
 -Wformat 
 -Wformat-security
ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/4.8"
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/4.8
 /usr/include/x86_64-linux-gnu/c++/4.8
 /usr/include/c++/4.8/backward
 /usr/lib/gcc/x86_64-linux-gnu/4.8/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/4.8/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "<stdin>"
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/4.8/:/usr/lib/gcc/x86_64-linux-gnu/4.8/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/4.8/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/4.8/:/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-E' '-v' '-mtune=generic' '-march=x86-64'

Lista :


Z użyciem tylko gcc [5]

 gcc -E -Wp,-v -xc /dev/null
 
 
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/11/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
# 0 "/dev/null"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "/dev/null"

Symbole

[edytuj]

Lista symboli compilatora :[6]

gcc -dM -E - < /dev/null

Plik wynikowy

[edytuj]

Za pomocą komendy file sprawdzamy plik wynikowy:


file a.out

Przykładowy wynik:

a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x4f7368b40e4eeee54307e8491e0f2ace1405d841, not stripped

Możemy również użyć komendy: readlf[7]

biblioteki

[edytuj]

Za pomocą komendy ldd sprawdzamy jakich bibliotek używa plik wynikowy:

ldd a.out

przykładowy wynik:

	linux-vdso.so.1 =>  (0x00007fff4ad68000)
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fc5c7758000)
	libgomp.so.1 => /usr/lib/x86_64-linux-gnu/libgomp.so.1 (0x00007fc5c7549000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc5c732b000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc5c6f63000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fc5c7a7f000)

flagi

[edytuj]
  • -L ( od ang Library) - pokazuje katalog gdzie są pliki binarne biblioteki
  • -E - powoduje wygenerowanie kodu programu ze zmianami, wprowadzonymi przez preprocesor
  • -S - zamiana kodu w języku C na kod asemblera (komenda: gcc -S plik.c spowoduje utworzenie pliku o nazwie plik.s, w którym znajdzie się kod asemblera)
  • -c - kompilacja bez łączenia z bibliotekami
  • -Ikatalog - ustawienie domyślnego katalogu z plikami nagłówkowymi na katalog, (duża litera i, od ang. Include)
  • -lbiblioteka - (mała litera l) : wymusza łączenie programu z podaną biblioteką = dołącza skompilowaną bibliotekę (np. -lGL)
  • -M = opcja preprocesora powodująca wypisanie ( dodanie do pliku Makefile) wymaganych opcji kompilacji. Jest używnaie przez gccmakedep program


Typowe zestawy flag

  • -Wall -Wextra -march=native -std=c99


std

[edytuj]

Opcje sterujące dialektem C[8]

  • std
  • pedantic
  • ansi


Domyślny standard języka zależy od wersji kompilatora[9]

march

[edytuj]

Mikroarchitektura procesora ( march )

Sprawdzamy za pomocą cat lub gcc : [10]

  gcc -c -Q -march=native --help=target

przykładowy wynik :

The following options are target specific:
  -m128bit-long-double        		[disabled]
  -m16                        		[disabled]
  -m32                        		[disabled]
  -m3dnow                     		[disabled]
  -m3dnowa                    		[disabled]
  -m64                        		[enabled]
  -m80387                     		[enabled]
  -m8bit-idiv                 		[disabled]
  -m96bit-long-double         		[enabled]
  -mabi=                      		sysv
  -mabm                       		[enabled]
  -maccumulate-outgoing-args  		[disabled]
  -maddress-mode=             		short
  -madx                       		[disabled]
  -maes                       		[enabled]
  -malign-data=               		compat
  -malign-double              		[disabled]
  -malign-functions=          		0
  -malign-jumps=              		0
  -malign-loops=              		0
  -malign-stringops           		[enabled]
  -mandroid                   		[disabled]
  -march=                     		haswell
  -masm=                      		att
  -mavx                       		[enabled]
  -mavx2                      		[enabled]
  -mavx256-split-unaligned-load 	[disabled]
  -mavx256-split-unaligned-store 	[disabled]
  -mavx512bw                  		[disabled]
  -mavx512cd                  		[disabled]
  -mavx512dq                  		[disabled]
  -mavx512er                  		[disabled]
  -mavx512f                   		[disabled]
  -mavx512ifma                		[disabled]
  -mavx512pf                  		[disabled]
  -mavx512vbmi                		[disabled]
  -mavx512vl                  		[disabled]
  -mbionic                    		[disabled]
  -mbmi                       		[enabled]
  -mbmi2                      		[enabled]
  -mbranch-cost=              		0
  -mcld                       		[disabled]
  -mclflushopt                		[disabled]
  -mclwb                      		[disabled]
  -mcmodel=                   		32
  -mcpu=                      		
  -mcrc32                     		[disabled]
  -mcx16                      		[enabled]
  -mdispatch-scheduler        		[disabled]
  -mdump-tune-features        		[disabled]
  -mf16c                      		[enabled]
  -mfancy-math-387            		[enabled]
  -mfentry                    		[enabled]
  -mfma                       		[enabled]
  -mfma4                      		[disabled]
  -mforce-drap                		[disabled]
  -mfp-ret-in-387             		[enabled]
  -mfpmath=                   		387
  -mfsgsbase                  		[enabled]
  -mfused-madd                		
  -mfxsr                      		[enabled]
  -mglibc                     		[enabled]
  -mhard-float                		[enabled]
  -mhle                       		[disabled]
  -mieee-fp                   		[enabled]
  -mincoming-stack-boundary=  		0
  -minline-all-stringops      		[disabled]
  -minline-stringops-dynamically 	[disabled]
  -mintel-syntax              		
  -mlarge-data-threshold=     		0x10000
  -mlong-double-128           		[disabled]
  -mlong-double-64            		[disabled]
  -mlong-double-80            		[enabled]
  -mlwp                       		[disabled]
  -mlzcnt                     		[enabled]
  -mmemcpy-strategy=          		
  -mmemset-strategy=          		
  -mmmx                       		[enabled]
  -mmovbe                     		[enabled]
  -mmpx                       		[disabled]
  -mms-bitfields              		[disabled]
  -mmwaitx                    		[disabled]
  -mno-align-stringops        		[disabled]
  -mno-default                		[disabled]
  -mno-fancy-math-387         		[disabled]
  -mno-push-args              		[disabled]
  -mno-red-zone               		[disabled]
  -mno-sse4                   		[disabled]
  -mnop-mcount                		[disabled]
  -momit-leaf-frame-pointer   		[disabled]
  -mpc32                      		[disabled]
  -mpc64                      		[disabled]
  -mpc80                      		[disabled]
  -mpclmul                    		[enabled]
  -mpcommit                   		[disabled]
  -mpopcnt                    		[enabled]
  -mprefer-avx128             		[disabled]
  -mpreferred-stack-boundary= 		0
  -mprefetchwt1               		[disabled]
  -mprfchw                    		[disabled]
  -mpush-args                 		[enabled]
  -mrdrnd                     		[enabled]
  -mrdseed                    		[disabled]
  -mrecip                     		[disabled]
  -mrecip=                    		
  -mrecord-mcount             		[disabled]
  -mred-zone                  		[enabled]
  -mregparm=                  		0
  -mrtd                       		[disabled]
  -mrtm                       		[disabled]
  -msahf                      		[enabled]
  -msha                       		[disabled]
  -mskip-rax-setup            		[disabled]
  -msoft-float                		[disabled]
  -msse                       		[enabled]
  -msse2                      		[enabled]
  -msse2avx                   		[disabled]
  -msse3                      		[enabled]
  -msse4                      		[enabled]
  -msse4.1                    		[enabled]
  -msse4.2                    		[enabled]
  -msse4a                     		[disabled]
  -msse5                      		
  -msseregparm                		[disabled]
  -mssse3                     		[enabled]
  -mstack-arg-probe           		[disabled]
  -mstack-protector-guard=    		tls
  -mstackrealign              		[enabled]
  -mstringop-strategy=        		[default]
  -mtbm                       		[disabled]
  -mtls-dialect=              		gnu
  -mtls-direct-seg-refs       		[enabled]
  -mtune-ctrl=                		
  -mtune=                     		haswell
  -muclibc                    		[disabled]
  -mveclibabi=                		[default]
  -mvect8-ret-in-mem          		[disabled]
  -mvzeroupper                		[disabled]
  -mx32                       		[disabled]
  -mxop                       		[disabled]
  -mxsave                     		[enabled]
  -mxsavec                    		[disabled]
  -mxsaveopt                  		[enabled]
  -mxsaves                    		[disabled]

  Known assembler dialects (for use with the -masm-dialect= option):
    att intel

  Known ABIs (for use with the -mabi= option):
    ms sysv

  Known code models (for use with the -mcmodel= option):
    32 kernel large medium small

  Valid arguments to -mfpmath=:
    387 387+sse 387,sse both sse sse+387 sse,387

  Known data alignment choices (for use with the -malign-data= option):
    abi cacheline compat

  Known vectorization library ABIs (for use with the -mveclibabi= option):
    acml svml

  Known address mode (for use with the -maddress-mode= option):
    long short

  Known stack protector guard (for use with the -mstack-protector-guard= option):
    global tls

  Valid arguments to -mstringop-strategy=:
    byte_loop libcall loop rep_4byte rep_8byte rep_byte unrolled_loop
    vector_loop

  Known TLS dialects (for use with the -mtls-dialect= option):
    gnu gnu2

Używamy :

 -march=native

lub tutaj

 -march=haswell

Plik :[11]

int main() {


  int a = 0;


  while (a < 100) {
    a++;
  }


  a = 0;
  do {
    a++;
  }while(a < 100);


  for ( a=0; a < 100; ++a){ 
    a++;}


  return 0;
}

Skompilujemy :


 gcc -S l.c -Wall

Oprócz pliku a.out otrzymamy dodatkowy plik z rozszerzeniem "s":

a.out  l.c  l.s

który zawiera kodem assemblera :

	.file	"l.c"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$0, -4(%rbp)
	jmp	.L2
.L3:
	addl	$1, -4(%rbp)
.L2:
	cmpl	$99, -4(%rbp)
	jle	.L3
	movl	$0, -4(%rbp)
.L4:
	addl	$1, -4(%rbp)
	cmpl	$99, -4(%rbp)
	jle	.L4
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 5.4.1-2ubuntu1~16.04) 5.4.1 20160904"
	.section	.note.GNU-stack,"",@progbits

v ( ustawienia)

[edytuj]

Użycie flagi v powoduje wydrukowanie na standardowym wyjściu błędu (ang. standard error output) komend uruchamianych w trakcie kompilacji oraz wersji programu kompilatora [12]

gcc -v

przykładowy wynik:

 Using built-in specs.
 COLLECT_GCC=gcc
 COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.8/lto-wrapper
 Target: x86_64-linux-gnu
 Configured with: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.8.1-10ubuntu9' --with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.8 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.8 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.8-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.8.1 (Ubuntu/Linaro 4.8.1-10ubuntu9)

wykrywacze błędów pamięci i wątków

[edytuj]

Od wersji 4.8 kompilatory C i C++ z GNU Compiler Collection są wyposażone we wbudowane wykrywacze błędów pamięci i wątków ( ang. Address and Thread Sanitizers) [13][14]

Rodzaje nieprawidłowych dostępów do pamięci, które można dziś wykryć, to:

  • Dostęp poza zakresem dla obiektów lokalnych, globalnych i sterty
  • Dostępy typu „use-after-free” dla obiektów sterty

Opcje instrumentacji programu ( ang. Program Instrumentation Options ) [15]

  • -fsanitize=style -fsanitize-recover -fsanitize-recover=style
  • -fasan-shadow-offset=number -fsanitize-sections=s1,s2,...
  • -fsanitize-undefined-trap-on-error -fbounds-check

Pomoc

[edytuj]

Offline :

man gcc

lub w postaci pliku tekstowego:[16]

man gcc | col -b > gcc.txt

lub info:

info gcc


Online :

Program gccmakedep tworzy zależności w plikach makefile za pomocą

gcc -M
gccmakedep

Kompilacja

[edytuj]

Aby skompilować kod języka C za pomocą kompilatora G++, napisany wcześniej w dowolnym edytorze tekstu, należy uruchomić program z odpowiednimi parametrami. Podstawowym parametrem, który jest wymagany, jest nazwa pliku zawierającego kod programu który chcemy skompilować.

gcc kod.c
Rezultatem kompilacji będzie plik wykonywalny, z domyślną nazwą (w systemach Unix jest to "a.out").

Jest to metoda niezalecana, ponieważ w przypadku, gdy skompilujemy w jednym katalogu kilka plików z kodem, kolejne pliki wykonywalne zostaną nadpisane i w rezultacie otrzymamy tylko jeden (ostatni) skompilowany kod. Aby wymusić na G++ nazwę pliku wykonywalnego musimy skorzystać z parametru "-o <nazwa>":

gcc -o program kod.c
W rezultacie otrzymujemy plik wykonywalny o nazwie program.

Pracując nad złożonym programem składającym się z kilku plików źródłowych (.c), możemy skompilować je niezależnie od siebie, tworząc tak zwane pliki typu obiekt, z rozszerzeniem .o (ang. Object File). Następnie możemy stworzyć z nich jednolity program w procesie konsolidacji (linkowaniu). Jest to bardzo wygodne i praktyczne rozwiązanie ze względu na to, iż nie jesteśmy zmuszeni kompilować wszystkich plików tworzących program za każdym razem na nowo, a jedynie te, w których wprowadziliśmy zmiany Aby skompilować plik bez linkowania używamy parametru "-c <plik>":

gcc -o program1.o -c kod1.c
gcc -o program2.o -c kod2.c

Otrzymujemy w ten sposób pliki typu obiekt program1.o i program2.o. A następnie tworzymy z nich program główny:

gcc -o program program1.o program2.o

Możemy użyć również flag, m.in. aby włączyć dokładne, rygorystyczne sprawdzanie napisanego kodu (co może być przydatne, jeśli chcemy dążyć do perfekcji), używamy przełączników:

gcc kod.c -o program -Werror -Wall -W -pedantic -ansi

Borland

[edytuj]

Zobacz podręcznik Borland C++ Compiler.

Błędy kompilacji

[edytuj]

Jedną z najbardziej podstawowych umiejętności, które musi posiąść początkujący programista jest umiejętność rozumienia komunikatów o różnego rodzaju błędach, które sygnalizuje kompilator. Wszystkie te informacje pomogą Ci szybko wychwycić ewentualne błędy (których na początku zawsze jest bardzo dużo). Nie martw się, że na początku dość często będziesz oglądał wydruki błędów, zasygnalizowanych przez kompilator - nawet zaawansowanym programistom się to zdarza. Kompilator ma za zadanie pomóc Ci w szybkiej poprawie ewentualnych błędów, dlatego też umiejętność analizy komunikatów o błędach jest tak ważna.

Zaczynamy analizę od pierwszego komunikatu kompilatora. Poprawiamy błąd i ponownie kompilujemy (może wtedy znikną pozostałe błędy).

GCC

[edytuj]

Komunikaty

[edytuj]

Język komunikatów zależy od zmiennych środowiska, np.:[17]

  • LANG
  • LC_CTYPE
  • LC_MESSAGES
  • LC_ALL


echo $LANG
pl_PL.UTF-8

Błędy składniowe

[edytuj]

Kompilator jest w stanie wychwycić błędy składniowe, które z pewnością będziesz popełniał. Kompilator GCC wyświetla je w następującej formie:

 nazwa_pliku.c:numer_linijki:opis błędu

Kompilator dość często podaje także nazwę funkcji, w której wystąpił błąd.


Przykładowo, błąd deklaracji zmiennej w pliku test.c:

#include <stdio.h>

int main ()
{
 intr r;
 printf ("%d\n", r);
}

Spowoduje wygenerowanie następującego komunikatu o błędzie:

test.c: In function ‘main’:
test.c:5: error: ‘intr’ undeclared (first use in this function)
test.c:5: error: (Each undeclared identifier is reported only once
test.c:5: error: for each function it appears in.)
test.c:5: error: syntax error before ‘r’
test.c:6: error: ‘r’ undeclared (first use in this function)

Co widzimy w raporcie o błędach?

W linii 5 użyliśmy nieznanego (undeclared) identyfikatora intr - kompilator mówi, że nie zna tego identyfikatora, jest to pierwsze użycie w danej funkcji i że więcej nie ostrzeże o użyciu tego identyfikatora w tej funkcji. Ponieważ intr nie został rozpoznany jako żaden znany typ, linijka intr r; nie została rozpoznana jako deklaracja zmiennej i kompilator zgłasza błąd składniowy (syntax error). W konsekwencji r nie zostało rozpoznane jako zmienna i kompilator zgłosi to jeszcze w następnej linijce, gdzie używamy r.


error: initializer element is not constant

Ten błąd powstaje gdy:

  • za pomocą jednej instrukcji deklarujemy i inicjujemy zmienną
  • do nadania wartości używamy wyrażenia a nie za stałej wartości (liczba)

Rozwiązaniem jest rozdzielenie deklaracji i inicjalizacji na 2 instrukcje.

Ostrzeżenie: niejawna deklaracja funkcji, np.:

t.c:696:17: warning: implicit declaration of function ‘dDrawLine’

powstanie gdy:

  • nie dołączymy odpowiedniej biblioteki zawierającej deklarację funkcji,
  • deklaracja własnej funkcji będzie zawarta w programie ale nie zostanie rozpoznana z powodu błędów składniowych


Inny błąd :

membership_new.c:63:9: error: expected ‘,’ or ‘;’ before ‘int’

przyczyna: w linii poprzedzającej linię 63 brak na końcu średnika (;) lub przecinka (,)

Błędy związane z bibliotekami

[edytuj]

linker

[edytuj]

Przykładowy komunikat:

/usr/bin/ld: cannot find -lXmu

Problem jest związany z opcją oznaczoną literą l linkera (ld):[18]

-llibrary

Linker nie może znaleźć biblioteki liblibrary, czyli w tym przypadku libXmu.

Sprawdzamy czy mamy taką biblioteką zainstalowaną, jeśli nie to szukamy pakietu libXmu np. w menadżerze pakietów Synaptic (w Ubuntu libXmu-dev) i instalujemy go.


Nowy błąd ( pojawił się po zainstalowanie clang)

/usr/bin/ld: cannot find -lstdc++: No such file or directory

Oznacza że link symboliczny ( ang. symbolic link) libstdc++.so wskazuje na nieistniejacy plik[19]

cd /usr/lib; ls

Możliwości

  • nie ma pliku libstdc++.so w katalogu /usr/lib/
  • są 2 pliki libstdc++.so
sudo apt-get update && sudo apt-get upgrade

Znajdujemy plik biblioteki libstdc++.so.6

sudo find / -name libstdc++.so.6

i tworzymy plik:

sudo ln -s /usr/lib/x86_64-linux-gnu/libstdc++.so.6 /usr/lib/libstdc++.so

undefined references

[edytuj]

Komunikat "undefined references" pochodzi głównie z linkera.

Przykładowy komunikat dotyczący biblioteki matematycznej: [20]

undefined reference to 'pow' collect2: error: ld returned 1 exit status

Może być spowodowany kolejnością linkowania bibliotek, np w pliku MAKFILE, dobra kolejność:

LIBS =  -lcairo -lmpfr -lm 

zła:

LIBS =  -lm -lcairo -lmpfr


Komunikat :

  undefined reference to symbol 'pow@@GLIBC_2.2.5'
  • przyczyna: ( użycie funkcji pow z biblioteki math
  • rozwiązanie : dodać -lm do opcji kompilacji

stray

[edytuj]
r1.c:194:3: error: stray ‘\342’ in program
r1.c:194:3: error: stray ‘\210’ in program
r1.c:194:3: error: stray ‘\222’ in program

Błąd ten jest spowodowany występowaniem w kodzie innych znaków niż ASCI[21]. Prawdopodobnie skopiowano znaki z innego programu, np.:

  • pliku pdf
  • pliku/programu Microsoft Word
  • kalkulatora gcalctool

Rozwiązanie:

  • usuń skopiowany tekst
  • wpisz go ręcznie

*** stack smashing detected ***: terminated

[edytuj]
  • You have a buffer overflow! [22]

Czy można określić lub ustawić opcje kompilatora z poziomu kodu źródłowego w gcc?

[edytuj]

Nie. Zamiast tego umieszczasz w źródle kod specyficzny dla kompilatora/platformy/systemu operacyjnego i otaczasz go odpowiednimi instrukcjami ifdef.

  
#ifdef __GNUC__
/*code for GNU C compiler */
#elif _MSC_VER
/*usually has the version number in _MSC_VER*/
/*code specific to MSVC compiler*/
#elif __BORLANDC__
/*code specific to borland compilers*/
#elif __MINGW32__
/*code specific to mingw compilers*/
#endif

Czy można wykryć flagi kompilacji GCC z pliku binarnego?

[edytuj]

GCC ma tę funkcję w wersji 4.3, jeśli zostanie o to poproszony podczas kompilacji kodu: Nowy przełącznik wiersza poleceń

-frecord-gcc-switches 

powoduje, że wiersz poleceń użyty do wywołania kompilatora zostanie zapisany w pliku obiektowym to się tworzy. Dokładny format tego nagrania zależy od formatu docelowego i pliku binarnego, ale zwykle ma formę sekcji notatki zawierającej tekst ASCII.

Zobacz również

[edytuj]

Źródła

[edytuj]
  1. GCC (ang. GNU Compiler Collection)
  2. nadeausoftware : C/C++ tip: How to detect the compiler name and version using compiler predefined macros
  3. gcc.gnu.org onlinedocs : Common-Predefined-Macros.html
  4. CONTROLLING #INCLUDE IN C++ by BRIAN FITZGERALD
  5. stackoverflow question: what-are-the-gcc-default-include-directories
  6. Commands using gcc
  7. Dive into ELF files using readelf command by Himanshu Arora
  8. gcc gnu ver. 13.2.0: C-Dialect-Options
  9. stackoverflow question: what-is-the-default-c-std-standard-version-for-the-current-gcc-especially-on-u
  10. gentoo wiki : GCC optimization march
  11. stackoverflow question : which-loop-is-faster-in-c-while-loop-or-do-while-loop
  12. Opcje gcc
  13. developers redhat blog: address-and-thread-sanitizers-gcc
  14. gavinchou : gcc-address-sanitizer
  15. gnu online docs : gcc-11.3.0 Instrumentation-Options
  16. GCC and Make by Chua Hock-Chuan
  17. 3.19 Environment Variables Affecting GCC
  18. Dokumentacja Gcc: 3.13 Options for Linking
  19. stackoverflow question: usr-bin-ld-cannot-find-lstdc-for-ubuntu-while-trying-to-swift-build-perfe
  20. [ http://stackoverflow.com/questions/12824134/undefined-reference-to-pow-in-c-despite-including-math-h Undefined reference to pow( ) in C, despite including math.h ]
  21. gcc: error: stray ‘\342’ in program - www.giannistsakiris.com
  22. quora : What-does-stack-smashing-detected-terminated-mean-How-can-we-correct-it?



Pierwszy program

[edytuj]

Jak napisać pierwszy program ?

Pierwszy program

[edytuj]

Pierwszy, minimalny program, który daje się skompilować i uruchomić:

Tworzymy katalog na nasz projekt. W katalogu tworzymy plik z rozszerzeniem ".c". W konsoli systemu Linux można to zrealizować następująco:

touch program_test_1.c

Następnie przechodzimy do edycji pliku przy pomocy edytora tekstowego ("programistycznego"). Można to wykonać np. edytorem Notepad++ (Windows) lub mcedit, vim, (Linux)

Wpisujemy do pliku treść programu

void main(){}

Należy zaznaczyć, że jest to postać skrajnie "skoncentrowana".

Kompilujemy program. W systemie Linux popularny jest kompilator gcc.

gcc program_test_1.c

Kompilator powinien utworzyć plik wykonywalny o domyślnej nazwie "a.out".

Uruchamiamy program. W konsoli systemu Linux piszemy:

./a.out

Co prawda program nic nie robi, ale możemy go traktować jako szkielet do dalszych prób.

W kompilatorze gcc możemy zmienić nazwę wydawanego programy przy pomocy flagi -o. Przykładowo w konsoli systemu Linux:

gcc program_test_1.c -o test_1.run

Możemy zrobić kod bardziej czytelnym

/*
* Każdy program języka C musi zawierać podprogram (funkcję) o nazwie "main". 
* Jest to pierwszy wykonywany fragment algorytmu
*/

/*
* funkcja "main" nie wydaje żadnych wartości, zatem jest typu "void"
* funkcja "main" nie pobiera żadnych parametrów, zatem ma "puste" nawiasy main()
*/
void
main()
{/* początek bloku funkcji "main" */

    /* miejsce na deklaracje zmiennych */
    /* brak */

    /* miejsce na instrukcje */
    /* brak */

}/* koniec bloku funkcji "main" */

Hello world!

[edytuj]

własne puts

[edytuj]
/*

https://www.quora.com/How-can-I-print-%E2%80%9Cwelcome%E2%80%9D-without-using-in-C
Steve Baker

*/ 

/*
* Marcin Kulbaka 2021
*/

/*
* Deklaracja funkcji "puts". 
* Funkcja służy do wypisywania danych na ekranie.
* Została ona już napisana i wchodzi w skład standardowej biblioteki C.
* Deklaracja podaje jednie nagłówek tej funkcji.
* Mówi, że przy "typowej" kompilacji będziemy korzystać z
* funkcji "puts" zawartej w jakiejś bibliotece (domyślnie - standardowej bibliotece C)
*/
int puts(const char *s);
 

/*
* Funkcja główna "main". Tutaj
*/

int 
main ()
{/* main */

    /*** miejsce na zmienne ***/
    /* brak */

    /*** miejsce na instrukcje ***/

    /* wypisz na ekranie napis przy pomocy funkcji "puts" */
    puts ( "Hello world " ) ;

    /* zakończ funkcję zwracając wartość 0 (domyślnie oznacza to poprawne wykonanie zadania) */
    return 0;

}/* main */

puts z biblioteki standardowej

[edytuj]

Przyjęło się, że pierwszy program napisany w dowolnym języku programowania powinien wyświetlić tekst "Hello World!" (Witaj Świecie!). Sam język C nie ma żadnych mechanizmów przeznaczonych do wprowadzania i wypisywania danych, możemy jednak skorzystać z dostępnych rozwiązań - w tym przypadku gotowej funkcji puts, która umieszcza podany tekst na "strumieniu wyjściowym programu", co da nam efekt wyświetlenia napisu na ekranie (podobnie jak w Pascalu używa się do tego procedur. Pascalowskim odpowiednikiem funkcji puts jest writeln).

Funkcja ta jest zawarta w zbiorze wielu gotowych funkcji zwanym standardową biblioteką C (ang. C Standard Library). W języku C deklaracje funkcji zawarte są w plikach nagłówkowych[1]. Taki plik dołączymy do naszego programu przy pomocy dyrektywy #include [2]. Interesuje nas plik stdio.h, zawierający niezbędny dla kompilatora opis funkcji puts, który dołączyć do programu możemy w poniższy sposób:

#include <stdio.h>

W programie definiujemy główną funkcję main, będzie to punkt startu dla naszego programu. Definicja funkcji zawiera jej nazwę, listę przyjmowanych argumentów, typ zwracanej wartości (wyniku) oraz kod funkcji.

int main (void) 
{
   return 0;
}

Nasza funkcja main zawiera tylko jedno polecenie do wykonania: zakończ funkcję z wynikiem 0. Liczba ta będzie zwrócona do systemu operacyjnego jako wynik działania programu. W pierwszej linijce oprócz nazwy funkcji zawarliśmy także informację o typie zwracanej wartości: int (integer), czyli liczba całkowita, a w nawiasach o nie przyjmowaniu żadnych parametrów (słowo void (pustka) w liście argumentów).

Kod funkcji umieszcza się w nawiasach klamrowych { i }, kolejne polecenia rozdzielone są średnikami.

Ostatecznie kod będzie wyglądał jak poniżej:

#include <stdio.h>
int main (void)
{
   puts ("Hello World!");
   return 0;
}

Napisy umieszcza się wewnątrz pary cudzysłowów. Teraz wystarczy program skompilować i uruchomić.

Kompilacja programu

[edytuj]

Dotychczas zobaczyliśmy najprostszą kompilację:

gcc plik_z_kodem_programu.c

Przy poprawnej kompilacji (Braku błędów) jest plik wykonywalny "a.out".

Opcja "-o" pozwala zmienić nazwę pliku wyjściowego.

Warto też używać opcji sygnalizujących możliwe błędy, pomimo zgodności z zasadami składni. Typową flagą jest "-Wall". Pozwala ona poznać więcej wątpliwości kompilatora co do naszego programu.

gcc -Wall program_1.c -o program_1.run

Wątpliwości dotyczące kompilacji można poznać nieraz przez poszukiwanie odpowiedzi w Internecie. Przykładem forum z masą pytań jest / było forum Stackoverflow i wyszukiwarki internetowe. Można znaleźć pytania dotyczące typowych problemów z kompilacją.

Nie ukrywam, że język C nie należy do najprostszych i znaczne przerabianie programu prowadzi np. do obecności niewykorzystywanych zmiennych. Stąd ważne jest precyzyjne zdefiniowanie celu kodowania. Jeżeli dobrze wiemy, co mamy zakodować, wówczas praca z językiem C może się zmienić zasadniczo. Problem z praktycznym realizowaniem języka C w swoich projektach jest generalnie znany w świecie.

Uruchomienie programu

[edytuj]

Linux

[edytuj]

Jeśli nie nadaliśmy innej nazwy przy kompilacji to standardowe wpisujemy w konsoli:

./a.out

Rozwiązywanie problemów

[edytuj]

Jeśli nie możesz skompilować powyższego programu, mogłeś popełnić literówkę przy przepisywaniu go. Więcej informacji o kompilacji tutaj.

Jeśli udało Ci się pomyślnie skompilować i uruchomić program, jest możliwe, że jedyne co zaobserwowałeś to mignięcie okienka konsoli. Dzieje się tak, ponieważ nasz program wraz z wykonaniem swojego zadania (wypisanie komunikatu) kończy działanie, nie czekając na reakcję użytkownika. Problem nie występuje, jeśli uruchomimy aplikację z poziomu linii poleceń (np. w oknie konsoli).

Dodajmy do naszego programu polecenie wczytania pojedynczego znaku z wejścia (w zwykłym przypadku oznacza to oczekiwanie, aż użytkownik wciśnie jakiś klawisz na klawiaturze) - zadanie to wykona funkcja getch.

#include <stdio.h>
int main (void)
{
   puts ("Hello World!");
   getch();
   return 0;
}

Tym razem program nie może zakończyć działania, póki nie wczyta znaku - dopiero po naciśnięciu dowolnego klawisza zamknie się okno konsoli (w przypadku zwykłego uruchomienia programu). Nasz pierwszy program gotowy!

Zobacz również

[edytuj]




Podstawowe wiadomości

[edytuj]

Dla właściwego zrozumienia języka C nieodzowne jest przyswojenie sobie pewnych ogólnych informacji.

Kompilacja: Jak działa C?

[edytuj]

Jak każdy język programowania, C sam w sobie jest niezrozumiały dla procesora. Został on stworzony w celu umożliwienia ludziom łatwego pisania kodu, który może zostać przetworzony na kod maszynowy. Program, który zamienia kod C na wykonywalny kod binarny, to kompilator. Jeśli pracujesz nad projektem, który wymaga kilku plików kodu źródłowego (np. pliki nagłówkowe), wtedy jest uruchamiany kolejny program - linker. Linker służy do połączenia różnych plików i stworzenia jednej aplikacji lub biblioteki (library). Biblioteka jest zestawem procedur, który sam w sobie nie jest wykonywalny, ale może być używana przez inne programy. Kompilacja i łączenie plików są ze sobą bardzo ściśle powiązane, stąd są przez wielu traktowane jako jeden proces. Jedną rzecz warto sobie uświadomić - kompilacja jest jednokierunkowa: przekształcenie kodu źródłowego C w kod maszynowy jest bardzo proste, natomiast odwrotnie - nie. Dekompilatory co prawda istnieją, ale rzadko tworzą użyteczny kod C.

Najpopularniejszym wolnym kompilatorem jest prawdopodobnie GNU Compiler Collection, dostępny na stronie gcc.gnu.org.

Co może C?

[edytuj]

Pewnie zaskoczy Cię to, że tak naprawdę "czysty" język C nie może zbyt wiele. Język C w grupie języków programowania wysokiego poziomu jest stosunkowo nisko. Dzięki temu kod napisany w języku C można dość łatwo przetłumaczyć na kod asemblera. Bardzo łatwo jest też łączyć ze sobą kod napisany w języku asemblera z kodem napisanym w C. Dla bardzo wielu ludzi przeszkodą jest także dość duża liczba i częsta dwuznaczność operatorów. Początkujący programista, czytający kod programu w C może odnieść bardzo nieprzyjemne wrażenie, które można opisać cytatem "ja nigdy tego nie opanuję". Wszystkie te elementy języka C, które wydają Ci się dziwne i nielogiczne w miarę, jak będziesz nabierał doświadczenia nagle okażą się całkiem przemyślanie dobrane i takie, a nie inne konstrukcje przypadną Ci do gustu. Dalsza lektura tego podręcznika oraz zaznajamianie się z funkcjami z różnych bibliotek ukażą Ci całą gamę możliwości, które daje język C doświadczonemu programiście.

Struktura blokowa

[edytuj]

Teraz omówimy podstawową strukturę programu napisanego w C. Jeśli miałeś styczność z językiem Pascal, to pewnie słyszałeś o nim, że jest to język programowania strukturalny. W C nie ma tak ścisłej struktury blokowej, mimo to jest bardzo ważne zrozumienie, co oznacza struktura blokowa. Blok jest grupą instrukcji, połączonych w ten sposób, że są traktowane jak jedna całość. W C, blok zawiera się pomiędzy nawiasami klamrowymi { }. Blok może także zawierać kolejne bloki.

Zawartość bloku. Generalnie, blok zawiera ciąg kolejno wykonywanych poleceń. Polecenia zawsze (z nielicznymi wyjątkami) kończą się średnikiem (;). W jednej linii może znajdować się wiele poleceń, choć dla zwiększenia czytelności kodu najczęściej pisze się pojedynczą instrukcję w każdej linii. Jest kilka rodzajów poleceń, np. instrukcje przypisania, warunkowe czy pętli. W dużej części tego podręcznika będziemy zajmować się właśnie instrukcjami.

Pomiędzy poleceniami są również odstępy - spacje, tabulacje oraz przejścia do następnej linii, przy czym dla kompilatora te trzy rodzaje odstępów mają takie samo znaczenie. Dla przykładu, poniższe trzy fragmenty kodu źródłowego, dla kompilatora są takie same:

printf("Hello world"); return 0;
printf("Hello world");
return 0;
printf("Hello world");



return 0;

W tej regule istnieje jednak jeden wyjątek. Dotyczy on stałych tekstowych. W powyższych przykładach stałą tekstową jest "Hello world". Gdy jednak rozbijemy ten napis, kompilator zasygnalizuje błąd:

printf("Hello
world");
return 0;

Należy tylko zapamiętać, że stałe tekstowe powinny zaczynać się i kończyć w tej samej linii (można ominąć to ograniczenie - więcej w rozdziale Napisy). Oprócz tego jednego przypadku dla kompilatora ma znaczenie samo istnienie odstępu, a nie jego wielkość czy rodzaj. Jednak stosowanie odstępów jest bardzo ważne, dla zwiększenia czytelności kodu - dzięki czemu możemy zaoszczędzić sporo czasu i nerwów, ponieważ znalezienie błędu (które się zdarzają każdemu) w nieczytelnym kodzie może być bardzo trudne.

Zasięg

[edytuj]

Pojęcie to dotyczy zmiennych (które przechowują dane przetwarzane przez program). W każdym programie (oprócz tych najprostszych) są zarówno zmienne wykorzystywane przez cały czas działania programu oraz takie, które są używane przez pojedynczy blok programu (np. funkcję). Na przykład, w pewnym programie w pewnym momencie jest wykonywane skomplikowane obliczenie, które wymaga zadeklarowania wielu zmiennych do przechowywania pośrednich wyników. Ale przez większą część tego działania te zmienne są niepotrzebne i zajmują tylko miejsce w pamięci - najlepiej gdyby to miejsce zostało zarezerwowane tuż przed wykonaniem wspomnianych obliczeń, a zaraz po ich wykonaniu zwolnione. Dlatego w C istnieją zmienne globalne oraz lokalne. Zmienne globalne mogą być używane w każdym miejscu programu, natomiast lokalne - tylko w określonym bloku czy funkcji (oraz blokach w nim zawartych). Generalnie - zmienna zadeklarowana w danym bloku jest dostępna tylko wewnątrz niego.

Funkcje

[edytuj]

Funkcje są ściśle związane ze strukturą blokową - funkcją jest po prostu blok instrukcji, który jest potem wywoływany w programie za pomocą pojedynczego polecenia. Zazwyczaj funkcja wykonuje pewne określone zadanie, np. we wspomnianym programie wykonującym pewne skomplikowane obliczenie.

Każda funkcja ma swoją nazwę, za pomocą której jest potem wywoływana w programie, oraz blok wykonywanych poleceń. Wiele funkcji pobiera pewne dane, czyli argumenty funkcji, wiele funkcji także zwraca pewną wartość po zakończeniu wykonywania. Dobrym nawykiem jest dzielenie dużego programu na zestaw mniejszych funkcji - dzięki temu będziesz mógł łatwiej odnaleźć błąd w programie.

Jeśli chcesz użyć jakiejś funkcji, to powinieneś wiedzieć:

  • jakie zadanie wykonuje dana funkcja,
  • jaki jest rodzaj wczytywanych argumentów i do czego są one potrzebne tej funkcji,
  • jaki jest rodzaj zwróconych danych i co one oznaczają.

W programach w języku C jedna funkcja ma szczególne znaczenie - jest to main(). Funkcję tę, zwaną funkcją główną, musi zawierać każdy program. W niej zawiera się główny kod programu i przekazywane są do niej argumenty, z którymi wywoływany jest program (jako parametry argc i argv). Więcej o funkcji main() dowiesz się później w rozdziale Funkcje.

Biblioteki standardowe

[edytuj]

Język C, w przeciwieństwie do innych języków programowania (np. Fortranu czy Pascala), nie posiada absolutnie żadnych słów kluczowych, które odpowiedzialne by były za obsługę wejścia i wyjścia. Może się to wydawać dziwne - język, który sam w sobie nie posiada podstawowych funkcji, musi być językiem o ograniczonym zastosowaniu. Jednak brak podstawowych funkcji wejścia-wyjścia jest jedną z największych zalet tego języka. Jego składnia opracowana jest tak, by można było bardzo łatwo przełożyć ją na kod maszynowy. To właśnie dzięki temu programy napisane w języku C są takie szybkie. Pozostaje jednak pytanie - jak umożliwić programom komunikację z użytkownikiem?

W 1983 roku, kiedy zapoczątkowano prace nad standaryzacją C, zdecydowano, że powinien być zestaw instrukcji identycznych w każdej implementacji C. Nazwano je Biblioteką Standardową (czasem nazywaną "libc"). Zawiera ona podstawowe funkcje, które umożliwiają wykonywanie takich zadań jak wczytywanie i zwracanie danych, modyfikowanie zmiennych łańcuchowych, działania matematyczne, operacje na plikach i wiele innych, jednak nie zawiera żadnych funkcji, które mogą być zależne od systemu operacyjnego czy sprzętu, jak grafika, dźwięk czy obsługa sieci. W programie "Hello World" użyto funkcji z biblioteki standardowej - puts, która wyświetla na ekranie sformatowany tekst.

Komentarze i styl

[edytuj]

Komentarze - to tekst włączony do kodu źródłowego, który jest pomijany przez kompilator i służy jedynie dokumentacji. W języku C, komentarze zaczynają się od

/*

a kończą

*/

Dobre komentowanie ma duże znaczenie dla rozwijania oprogramowania, nie tylko dlatego, że inni będą kiedyś potrzebowali przeczytać napisany przez ciebie kod źródłowy, ale także możesz chcieć po dłuższym czasie powrócić do swojego programu, i możesz zapomnieć, do czego służy dany blok kodu, albo dlaczego akurat użyłeś tego polecenia, a nie innego. W chwili pisania programu, to może być dla ciebie oczywiste, ale po dłuższym czasie możesz mieć problemy ze zrozumieniem własnego kodu. Jednak nie należy też wstawiać zbyt dużo komentarzy, ponieważ wtedy kod może stać się jeszcze mniej czytelny - najlepiej komentować fragmenty, które nie są oczywiste dla programisty oraz te o szczególnym znaczeniu. Ale tego nauczysz się już w praktyce.

Dobry styl pisania kodu jest o tyle ważny, że powinien on być czytelny i zrozumiały; po to w końcu wymyślono języki programowania wysokiego poziomu (w tym C), aby kod było łatwo zrozumieć ;). I tak - należy stosować wcięcia dla odróżnienia bloków kolejnego poziomu (zawartych w innym bloku; podrzędnych), nawiasy klamrowe otwierające i zamykające blok powinny mieć takie same wcięcia, staraj się, aby nazwy funkcji i zmiennych kojarzyły się z zadaniem, jakie dana funkcja czy zmienna pełni w programie. W dalszej części podręcznika możesz napotkać więcej zaleceń dotyczących stylu pisania kodu. Staraj się stosować do tych zaleceń - dzięki temu kod pisanych przez ciebie programów będzie łatwiejszy do czytania i zrozumienia.

Innym zastosowaniem komentarzy jest chwilowe usuwanie fragmentów kodu. Jeśli część programu źle działa i chcemy ją chwilowo wyłączyć, albo fragment kodu jest nam już niepotrzebny, ale mamy wątpliwości, czy w przyszłości nie będziemy chcieli go użyć - umieszczamy go po prostu wewnątrz komentarza.

Podczas obejmowania chwilowo niepotrzebnego kodu w komentarz trzeba uważać na jedną subtelność. Otóż komentarze /* * /' w języku C nie mogą być zagnieżdżone. Trzeba na to uważać, gdy chcemy objąć komentarzem obszar w którym już istnieje komentarz (należy wtedy usunąć wewnętrzny komentarz). W nowszym standardzie C dopuszcza się, aby komentarz typu /* */ zawierał w sobie komentarz // i żeby komentarz typu "//" mógł być stosowany.

Po polsku czy angielsku?

[edytuj]

Jak już wcześniej było wspomniane, zmiennym i funkcjom powinno się nadawać nazwy, które odpowiadają ich znaczeniu. Zdecydowanie łatwiej jest czytać kod, gdy średnią liczb przechowuje zmienna srednia niż a, a znajdowaniem maksimum w ciągu liczb zajmuje się funkcja max albo znajdz_max niż nazwana f. Często nazwy funkcji to właśnie czasowniki.

Powstaje pytanie, w jakim języku należy pisać nazwy. Jeśli chcemy, by nasz kod mogły czytać osoby nieznające polskiego - warto użyć języka angielskiego. Jeśli nie - można bez problemu użyć polskiego. Bardzo istotne jest jednak, by nie mieszać języków. Jeśli zdecydowaliśmy się używać polskiego, używajmy go od początku do końca; przeplatanie ze sobą dwóch języków robi złe wrażenie.

Warto również zdecydować się na sposób zapisywania nazw składających się z więcej niż jednego słowa. Istnieje kilka możliwości, najważniejsze z nich:

  1. oddzielanie podkreśleniem: int_to_str
  2. "konwencja pascalowska", każde słowo dużą literą: IntToStr
  3. "konwencja wielbłądzia", pierwsze słowo małą, kolejne dużą literą: intToStr

Ponownie, najlepiej stosować konsekwentnie jedną z konwencji i nie mieszać ze sobą kilku.

Notacja węgierska

[edytuj]

Czasem programista może zapomnieć, jakiego typu była dana zmienna. Wtedy musi znaleźć odpowiednią deklarację (co nie zawsze jest łatwe). Dlatego więc wymyślono sposób, by temu zaradzić. Pomyślano, by w nazwie zmiennej (bądź wskaźnika na zmienną) napisać, jakiego jest ona typu, np:

  • a_liczba (liczba typu int)
  • w_ll_dlugaLiczba (wskaźnik na zmienną typu long long)
  • t5x5_ch_tabliczka (tablica 5x5 elementów typu char)
  • func_i_silnia (funkcja zwracająca int)

Jest to bardzo wygodne przy bardzo zagmatwanych zmiennych:

  • w_t4_w_t2x2_s_pomieszaniec (wskaźnik na tablicę czterech wskaźników na tablice dwuwymiarowe zmiennych typu short)

Lub gdy nie pamiętamy wymiarów tablicy:

  • t4x5x6_f_powalonaKostkaRubika (od razu wiemy, że t4x5x6_f_powalonaKostkaRubika[5][4][6] jest niewłaściwe)

Taki zapis ma też swoje wady. Gdy zdecydujemy się zmienić typ zmiennej, zamiast po prostu przemienić w deklaracji int na long, musimy zmieniać nazwy w całym programie. Często takie nazwy są po prostu długie i nie chce nam się ich pisać (no cóż, programista też człowiek), więc wolimy wprowadzić pomieszaniec zamiast w_t4_w_t2x2_s_pomieszaniec. Najważniejsze to jednak trzymać się rozwiązania, które wybraliśmy na początku, bo mieszanie jest przerażające.

Preprocesor

[edytuj]

Nie cały napisany przez ciebie kod będzie przekształcany przez kompilator bezpośrednio na kod wykonywalny programu. W wielu przypadkach będziesz używać poleceń "skierowanych do kompilatora", tzw. dyrektyw kompilacyjnych. Na początku procesu kompilacji, specjalny podprogram, tzw. preprocesor, wyszukuje wszystkie dyrektywy kompilacyjne i wykonuje odpowiednie akcje - które polegają notabene na edycji kodu źródłowego (np. wstawieniu deklaracji funkcji, zamianie jednego ciągu znaków na inny). Właściwy kompilator, zamieniający kod C na kod wykonywalny, nie napotka już dyrektyw kompilacyjnych, ponieważ zostały one przez preprocesor usunięte, po wykonaniu odpowiednich akcji.

W C dyrektywy kompilacyjne zaczynają się od znaku hash (#). Przykładem najczęściej używanej dyrektywy, jest #include, która jest użyta nawet w tak prostym programie jak "Hello, World!". #include nakazuje preprocesorowi włączyć (ang. include) w tym miejscu zawartość podanego pliku, tzw. pliku nagłówkowego; najczęściej to będzie plik zawierający funkcje z którejś biblioteki standardowej (stdio.h - STandard Input-Output, rozszerzenie .h oznacza plik nagłówkowy C). Dzięki temu, zamiast wklejać do kodu swojego programu deklaracje kilkunastu, a nawet kilkudziesięciu funkcji, wystarczy wpisać jedną magiczną linijkę!

Nazwy zmiennych, stałych i funkcji

[edytuj]

Identyfikatory, czyli nazwy zmiennych, stałych i funkcji mogą składać się z liter (bez polskich znaków), cyfr i znaku podkreślenia z tym, że nazwa taka nie może zaczynać się od cyfry. Nie można używać nazw zarezerwowanych (patrz: Składnia).

Przykłady błędnych nazw:

 2liczba      (nie można zaczynać nazwy od cyfry)
 moja funkcja (nie można używać spacji)
 $i           (nie można używać znaku $)
 if           (if to słowo kluczowe)

Aby kod był bardziej czytelny, przestrzegajmy poniższych (umownych) reguł:

  • nazwy zmiennych piszemy małymi literami: i, file
  • nazwy stałych (zadeklarowanych przy pomocy #define) piszemy wielkimi literami: SIZE
  • nazwy funkcji piszemy małymi literami: print
  • wyrazy w nazwach oddzielamy jedną z konwencji:
    • oddzielanie podkreśleniem: open_file
    • konwencja pascalowska: OpenFile
    • konwencja wielbłądzia: openFile




Zmienne w C

[edytuj]

Procesor komputera stworzony jest tak, aby przetwarzał dane, znajdujące się w pamięci komputera. Z punktu widzenia programu napisanego w języku C (który jak wiadomo jest językiem niskiego poziomu) dane umieszczane są w postaci tzw. zmiennych, stałych i literałów. Zmienne ułatwiają programiście pisanie programu. Dzięki nim programista nie musi się przejmować gdzie w pamięci owe zmienne się znajdują, tzn. nie operuje fizycznymi adresami pamięci, jak np. 0x14613467, tylko prostą do zapamiętania nazwą zmiennej.

Koncepcje C 1999 ISO

Czym są zmienne?

[edytuj]

Zmienna jest to pewien fragment pamięci o ustalonym rozmiarze, który posiada własny identyfikator (nazwę) oraz może przechowywać pewną wartość, zależną od typu zmiennej.


Deklaracja, definicja zmiennej[3]

Typ zmiennej

[edytuj]

Typ zmiennej[4]

Typy wg złożoności

Deklaracja zmiennych

[edytuj]

Aby móc skorzystać ze zmiennej należy ją przed użyciem zadeklarować, to znaczy poinformować kompilator, jak zmienna będzie się nazywać i jaki typ ma mieć. Zmienne deklaruje się w sposób następujący:

typ nazwa_zmiennej;

Oto deklaracja zmiennej o nazwie "wiek" typu "int" czyli liczby całkowitej:

int wiek;

Zmiennej w momencie zadeklarowania można od razu przypisać wartość:

int wiek = 17;
{
   int wiek = 17;
   printf("%d\n", wiek);
   int kopia_wieku; /* tu stary kompilator C zgłosi błąd - deklaracja występuje po instrukcji (printf). */
   kopia_wieku = wiek;
}

Według nowszych standardów (C99) możliwe jest deklarowanie zmiennej w dowolnym miejscu programu (podobnie jak w języku C++), ale wtedy musimy pamiętać, aby zadeklarować zmienną przed jej użyciem. To znaczy, że taki kod jest niepoprawny:

{
   printf ("Przeliczam wiek...\n");
   printf ("Mam %d lat\n", wiek);
   int wiek = 17; /* deklaracja po użyciu - kompilator nigdy tego nie dopuści */
}

Należy go zapisać tak:

{
   printf ("Przeliczam wiek...\n");
   int wiek = 17; /* deklaracja w środku bloku - dopuszczalna w C99 */
   printf ("Mam %d lat\n", wiek);
}


Jak czytać deklarację ?

[edytuj]
  • zasada spirali prawoskrętnej ( ang. The Clockwise/Spiral Rule)[5]
  • zasady wg Steve Friedl'a[6]
  • użyć programu cdecl: konwersja z c do języka angielskiego i w odwrotnym kierunku

Nazwa zmiennej

[edytuj]

Zasady nazywania zmiennych (ang. Rules for Constructing Variable Names in C Language) [7]

  • zestaw dopuszczalnych znaków
  • pierwszy znak : litera lub podkreślenie ( nie zalecane z uwagi na używanie ich w nazwach zmiennych systemowych)[8]
  • nie używamy słów kluczowych ( już są użyte ). Uwaga: po zmianie wielkości liter będą dopuszczalne
  • wielkość liter odróżnia nazwy ( ang. case sensitive)
  • długość nazwy
    • do pierwszych 8 znaków
    • do pierwszych 31 znaków
    • niektóre kompilatory/OS pozwalają na użycie do 247 znaków[9]

Przykłady niedopuszczalnych nazw:[10][11]

  1x  // zaczyna się od cyfry
  char // słowo zarezerwowane ( ang. reserved word ) 
  x+y //  znak specjalny
  addition of program // spacji używać nie wolno
  

Cel i zasady nadawania nazw:[12]

  • Wybierz słowo mające znaczenie (podaj kontekst): jednoznacznie i precyzyjnie opisywać koncept który nazywają[13]
  • Unikaj nazw ogólnych (takich jak tmp)
  • nazwa zmiennej nie może być taka sama jak słowo kluczowe języka C oraz jak nazwa innej zmiennej, która została wcześniej zdefiniowana w programie[14]
  • Dołącz dodatkowe informacje do nazwy (użyj sufiksu lub prefiksu)
  • Dołącz dodatkowe informacje do nazwy (użyj sufiksu lub prefiksu), zobacz notacja węgierska[15]
  • Nie rób zbyt długich ani zbyt krótkich imion
  • Używaj spójnego formatowania

Zasięg zmiennej

[edytuj]

Zmienne mogą być dostępne dla wszystkich funkcji programu - nazywamy je wtedy zmiennymi globalnymi. Deklaruje się je przed wszystkimi funkcjami programu:

#include <stdio.h>

int a,b; /* nasze zmienne globalne */

void func1 ()
{
 /* instrukcje */
 a=3;
 /* dalsze instrukcje */
}
 
int main ()
{
 b=3;
 a=2;
 return 0;
}

Specyfikacja języka C mówi, że zmienne globalne, jeśli programista nie przypisze im innej wartości podczas definiowania, są inicjalizowane wartością 0.

Zmienne, które funkcja deklaruje do "własnych potrzeb" nazywamy zmiennymi lokalnymi. Nasuwa się pytanie: "czy będzie błędem nazwanie tą samą nazwą zmiennej globalnej i lokalnej?". Otóż odpowiedź może być zaskakująca: nie. Natomiast w danej funkcji da się używać tylko jej zmiennej lokalnej. Tej konstrukcji należy, z wiadomych względów, unikać.

int a=1; /* zmienna globalna */ 

int main()
{
 int a=2;         /* to już zmienna lokalna */
 printf("%d", a); /* wypisze 2 */
}

Czas życia

[edytuj]

Czas życia to czas od momentu przydzielenia dla zmiennej miejsca w pamięci (stworzenie obiektu) do momentu zwolnienia miejsca w pamięci (likwidacja obiektu).

Zakres ważności to część programu, w której nazwa znana jest kompilatorowi.

main()
{
 int a = 10;
 {                            /* otwarcie lokalnego bloku */
   int b = 10;
   printf("%d %d", a, b);
 }                            /* zamknięcie lokalnego bloku, zmienna b jest usuwana */

 printf("%d %d", a, b);       /* BŁĄD: b juz nie istnieje */
}                               /* tu usuwana jest zmienna a */

Zdefiniowaliśmy dwie zmienne typu int. Zarówno a i b istnieją przez cały program (czas życia). Nazwa zmiennej a jest znana kompilatorowi przez cały program. Nazwa zmiennej b jest znana tylko w lokalnym bloku, dlatego nastąpi błąd w ostatniej instrukcji.

Możemy świadomie ograniczyć ważność zmiennej do kilku linijek programu (tak jak robiliśmy wyżej) tworząc blok. Nazwa zmiennej jest znana tylko w tym bloku.

{
  ...
}

Stałe

[edytuj]

Stała, różni się od zmiennej tylko tym, że nie można jej przypisać innej wartości w trakcie działania programu. Wartość stałej ustala się w kodzie programu i nigdy ona nie ulega zmianie.

W przeciwieństwie do języka C++, w C stała to cały czas zmienna, której kompilator pilnuje, by nie zmieniła się.

Stałą deklaruje się:

  • z użyciem słowa kluczowego const
  • dyrektywy preprocesora #define

Użycie stałej zapobiega używaniu magicznych liczb ( ang. Magic number or Unnamed numerical constants)

const

[edytuj]
const typ nazwa_stałej=wartość;

Dobrze jest używać stałych w programie, ponieważ unikniemy wtedy przypadkowych pomyłek a kompilator może często zoptymalizować ich użycie (np. od razu podstawiając ich wartość do kodu).

const int WARTOSC_POCZATKOWA=5;
int i=WARTOSC_POCZATKOWA;
WARTOSC_POCZATKOWA=4;  /* tu kompilator zaprotestuje */
int j=WARTOSC_POCZATKOWA;

Przykład pokazuje dobry zwyczaj programistyczny, jakim jest zastępowanie umieszczonych na stałe w kodzie liczb stałymi. W ten sposób będziemy mieli większą kontrolę nad kodem - stałe umieszczone w jednym miejscu można łatwo modyfikować, zamiast szukać po całym kodzie liczb, które chcemy zmienić.

Nie mamy jednak pełnej gwarancji, że stała będzie miała tę samą wartość przez cały czas wykonania programu, możliwe jest bowiem dostanie się do wartości stałej (miejsca jej przechowywania w pamięci) pośrednio - za pomocą wskaźników. Można zatem dojść do wniosku, że słowo kluczowe const służy tylko do poinformowania kompilatora, aby ten nie zezwalał na jawną zmianę wartości stałej. Z drugiej strony, zgodnie ze standardem, próba modyfikacji wartości stałej ma niezdefiniowane działanie (tzw. undefined behaviour) i w związku z tym może się powieść lub nie, ale może też spowodować jakieś subtelne zmiany, które w efekcie spowodują, że program będzie źle działał.

define

[edytuj]

Podobnie do zdefiniowania stałej możemy użyć dyrektywy preprocesora #define (opisanej w dalszej części podręcznika). Tak zdefiniowaną stałą nazywamy stałą symboliczną. W przeciwieństwie do stałej zadeklarowanej z użyciem słowa const stała zdefiniowana przy użyciu #define jest zastępowana daną wartością w każdym miejscu, gdzie występuje, dlatego też może być używana w miejscach, gdzie "normalna" stała nie mogłaby dobrze spełnić swej roli.

Literały

[edytuj]

Literały[16], czyli stałe dosłowne.

Zawsze w programie, w którym określasz wartość jawnie zamiast odnosić się do zmiennej lub innej formy danych, ta wartość jest określana jako literał.

Literały mogą przybierać formę zdefiniowaną przez ich typ :

  • całkowity
  • zmiennopozycyjny
  • znakowy
  • napisowy
  • złożony[17]
int ilosc = 23; // 23 to literał calkowity
double wysokosc = 88.2; // 88.2 to literał zmiennopozycyjny
char znak = 'c'; // 'c' jest literałem znakowym
string napis = "Napis"; // "Napis" to literał napisowy
int *p = (int []){2, 4, 6}; // literał złożony

Można użyć notacji szesnastkowej (szesnastkowej), aby bezpośrednio wstawić dane do zmiennej niezależnie od jej typu.

Podstawowe typy zmiennych

[edytuj]

Każdy program w C operuje na zmiennych - wydzielonych w pamięci komputera obszarach, które mogą reprezentować obiekty nam znane, takie jak liczby, znaki, czy też bardziej złożone obiekty. Jednak dla komputera każdy obszar w pamięci jest taki sam - to ciąg zer i jedynek, w takiej postaci zupełnie nieprzydatny dla programisty i użytkownika. Podczas pisania programu musimy wskazać, w jaki sposób ten ciąg ma być interpretowany.

Typ zmiennej wskazuje właśnie sposób, w jaki pamięć, w której znajduje się zmienna będzie wykorzystywana. Określając go przekazuje się kompilatorowi informację, ile pamięci trzeba zarezerwować dla zmiennej, a także w jaki sposób wykonywać na niej operacje.

Każda zmienna musi mieć określony swój typ w miejscu deklaracji i tego typu nie może już zmienić. Lecz co jeśli mamy zmienną jednego typu, ale potrzebujemy w pewnym miejscu programu innego typu danych? W takim wypadku stosujemy konwersję (rzutowanie) jednej zmiennej na inną zmienną. Rzutowanie zostanie opisane później, w rozdziale Operatory.



W języku C wyróżniamy następujące typy zmiennych ( wg wielkości ) :

  • 4 podstawowe :
    • char - jednobajtowe liczby całkowite, służy do przechowywania znaków;
    • int- typ całkowity, o długości domyślnej dla danej architektury komputera;
    • float - typ zmiennopozycyjny (zwany również zmiennoprzecinkowym), reprezentujący liczby rzeczywiste (4 bajty); Są dokładnie opisane w IEEE 754.
    • double - typ zmiennopozycyjny podwójnej precyzji (8 bajtów);
  • dodatkowe :
    • typ logiczny : bool (tylko C99) (wymaga dołączenia stdbool.h) - typ logiczny
    • typy złożone


Wg lokalizacji definicji typy dzielimy na :

  • wbudowane, które zna kompilator; są one w nim bezpośrednio "zaszyte"
  • zdefiniowane przez użytkownika typy danych. Należy je kompilatorowi opisać. Więcej informacji znajduje się w rozdziale Typy złożone.

Wg zastosowania typy możemy podzielić na :


Rozmiar zmiennych można sprawdzić za pomocą prostego programu


Pojęcia związane z typami:[18]

  • rodzaje typów
  • grupy typów
  • zgodność
  • konwersje
  • rozmiar



int

[edytuj]

Ten typ przeznaczony jest do liczb całkowitych. Liczby te możemy zapisać na kilka sposobów:

  • System dziesiętny
12 ; 13 ; 45 ; 35 itd
  • System ósemkowy (oktalny)
010    czyli 8
016    czyli 8 + 6 = 14
018    BŁĄD

System ten operuje na cyfrach od 0 do 7. Tak wiec 8 jest niedozwolona. Jeżeli chcemy użyć takiego zapisu musimy zacząć liczbę od 0.

  • System szesnastkowy (heksadecymalny)
0x10   czyli 1*16 + 0 = 16
0x12   czyli 1*16 + 2 = 18
0xff   czyli 15*16 + 15 = 255

W tym systemie możliwe cyfry to 0...9 i dodatkowo a, b, c, d, e, f, które oznaczają 10, 11, 12, 13, 14, 15. Aby użyć takiego systemu musimy poprzedzić liczbę ciągiem 0x. Wielkość znaków w takich literałach nie ma znaczenia.

W pliku inttypes.h są (od C99) zdefiniowane makra dla liczb całkowitych o stałej szerokości ( ang. Fixed width integer types ) [19]. Podstawowe typy:

  • int8_t
  • int16_t
  • int32_t
  • int64_t
  • uintptr_t = typ liczby całkowitej bez znaku, który może pomieścić wskaźnik[20]

Użycie typów o stałej szerokości zwiększa przenośność programów[21]

Zobacz:

float

[edytuj]

Ten typ oznacza liczby zmiennoprzecinkowe czyli ułamki. Istnieją dwa sposoby zapisu:

  • System dziesiętny
  3.14 ; 45.644 ; 23.54 ; 3.21 itd
  • System "naukowy" - wykładniczy
  pek = p*10^k = p*pow(10.0, k) 

przykłady:

  6e2 czyli 6 * 102 czyli 600
  1.5e3 czyli 1.5 * 103 czyli 1500
  3.4e-3 czyli 3.4 * 10(-3) czyli 0.0034

Należy wziąć pod uwagę, że reprezentacja liczb rzeczywistych w komputerze jest niedoskonała i możemy otrzymywać wyniki o zauważalnej niedokładności.[22]

double

[edytuj]

Double - czyli "podwójny" - oznacza liczby zmiennoprzecinkowe podwójnej precyzji. Oznacza to, że liczba taka zajmuje zazwyczaj w pamięci dwa razy więcej miejsca niż float (np. 64 bity wobec 32 dla float), ale ma też dwa razy wyższą dokładność.

Domyślnie ułamki wpisane w kodzie są typu double. Możemy to zmienić dodając na końcu literę "f":

  1.5f   (float)
  1.5    (double)


Specjalne wartości

[edytuj]

Można[23] wyświetlić wewnętrzną reprezentację ( binarną i 16-ową) wartości specjalnych[24] ( dla porównania kilka niespecjalnych):

0.5:
0 01111111110 0000000000000000000000000000000000000000000000000000
0 3FE 0

0.1:
0 01111111011 1001100110011001100110011001100110011001100110011010
0 3FB 999999999999A

0.0:
0 00000000000 0000000000000000000000000000000000000000000000000000
0 0 0

NaN:
1 11111111111 1000000000000000000000000000000000000000000000000000
1 7FF 8000000000000

+infinity:
0 11111111111 0000000000000000000000000000000000000000000000000000
0 7FF 0

2^-1074:
0 00000000000 0000000000000000000000000000000000000000000000000001
0 0 1

char

[edytuj]

Jest to typ znakowy, umożliwiający zapis znaków ASCII. Może też być traktowany jako liczba z zakresu 0..255. Znaki zapisujemy w pojedynczych cudzysłowach (czasami nazywanymi apostrofami), by odróżnić je od łańcuchów tekstowych (pisanych w podwójnych cudzysłowach).

   'a' ; '7' ; '!' ; '$'

Pojedynczy cudzysłów ' zapisujemy tak: '\'' a null (czyli zero, które między innymi kończy napisy) tak: '\0'. Więcej znaków specjalnych.

Warto zauważyć, że typ char to zwykły typ liczbowy i można go używać tak samo jak typu int (zazwyczaj ma jednak mniejszy zakres). Co więcej literały znakowe (np. 'a') są traktowane jako liczby i w języku C są typu int (w języku C++ są typu char).

Program[25] drukuje znaki drukowalne ASCII:

// https://en.cppreference.com/w/c/language/ascii
// printtable ASCII
#include <stdio.h>
 
int main(void)
{
    puts("Printable ASCII:");
    for (int i = 32; i < 127; ++i) {
        putchar(i);
        putchar(i % 16 == 15 ? '\n' : ' ');
    }
}


Wynik:

Printable ASCII:
  ! " # $ % & ' ( ) * + , - . /
0 1 2 3 4 5 6 7 8 9 : ; < = > ?
@ A B C D E F G H I J K L M N O
P Q R S T U V W X Y Z [ \ ] ^ _
` a b c d e f g h i j k l m n o
p q r s t u v w x y z { | } ~


Wewnętrzna reprezentacja znaku:[26]

  printf( "%c = %hhu\n", c, ( unsigned char )c );

void

[edytuj]

Słowa kluczowego void można w określonych sytuacjach użyć tam, gdzie oczekiwana jest nazwa typu. void nie jest właściwym typem, bo nie można utworzyć zmiennej takiego typu; jest to "pusty" typ (ang. void znaczy "pusty"). Typ void przydaje się do zaznaczania, że funkcja nie zwraca żadnej wartości lub że nie przyjmuje żadnych parametrów (więcej o tym w rozdziale Funkcje). Można też tworzyć zmienne będące typu "wskaźnik na void"

Specyfikatory

[edytuj]

Specyfikatory to słowa kluczowe, które postawione przy typie danych zmieniają jego znaczenie.

signed i unsigned

[edytuj]

Na początku zastanówmy się, jak komputer może przechować liczbę ujemną. Otóż w przypadku przechowywania liczb ujemnych musimy w zmiennej przechować jeszcze jej znak. Jak wiadomo, zmienna składa się z szeregu bitów. W przypadku użycia zmiennej pierwszy bit z lewej strony (nazywany także bitem najbardziej znaczącym) przechowuje znak liczby. Efektem tego jest spadek "pojemności" zmiennej, czyli zmniejszenie największej wartości, którą możemy przechować w zmiennej.

Signed oznacza liczbę ze znakiem, unsigned - bez znaku (nieujemną). Mogą być zastosowane do typów: char i int i łączone ze specyfikatorami short i long (gdy ma to sens).

Jeśli przy signed lub unsigned nie napiszemy, o jaki typ nam chodzi, kompilator przyjmie wartość domyślną czyli int.

Przykładowo dla zmiennej char(zajmującej 8 bitów zapisanej w formacie uzupełnień do dwóch) wygląda to tak:

signed char a;      /* zmienna a przyjmuje wartości od -128 do 127 */
unsigned char b;    /* zmienna b przyjmuje wartości od 0 do 255    */
unsigned short c;
unsigned long int d;

Jeżeli nie podamy żadnego ze specyfikatora wtedy liczba jest domyślnie przyjmowana jako signed (nie dotyczy to typu char, dla którego jest to zależne od kompilatora).

signed int i = 0;
// jest równoznaczne z:
int i = 0;

Liczby bez znaku pozwalają nam zapisać większe liczby przy tej samej wielkości zmiennej - ale trzeba uważać, by nie zejść z nimi poniżej zera - wtedy "przewijają" się na sam koniec zakresu, co może powodować trudne do wykrycia błędy w programach.

size_t

[edytuj]

Typ size_t jest zdefiniowany w nagłówku stddef.h[27][28] jako alias do liczby całkowitej bez znaku


typedef unsigned int size_t;

Użycie size_t może poprawić przenośność, wydajność i czytelność kodu.[29]

Możemy to sprawdzić :[30]

echo | gcc -E -xc -include 'stddef.h' - | grep size_t

przykładowy wynik :

typedef long unsigned int size_t;

short i long

[edytuj]

Short i long są wskazówkami dla kompilatora, by zarezerwował dla danego typu mniej (odpowiednio — więcej) pamięci. Mogą być zastosowane do dwóch typów: int i double (tylko long), mając różne znaczenie.

Jeśli przy short lub long nie napiszemy, o jaki typ nam chodzi, kompilator przyjmie wartość domyślną czyli int.

Należy pamiętać, że to jedynie życzenie wobec kompilatora - w wielu kompilatorach typy int i long int mają ten sam rozmiar. Standard języka C nakłada jedynie na kompilatory następujące ograniczenia:

int - nie może być krótszy niż 16 bitów;
int - musi być dłuższy lub równy short a nie może być dłuższy niż long;
short int - nie może być krótszy niż 16 bitów;
long int - nie może być krótszy niż 32 bity;

Zazwyczaj typ int jest typem danych o długości odpowiadającej wielkości rejestrów procesora, czyli na procesorze szesnastobitowym ma 16 bitów, na trzydziestodwubitowym - 32 itd.[31] Z tego powodu, jeśli to tylko możliwe, do reprezentacji liczb całkowitych preferowane jest użycie typu int bez żadnych specyfikatorów rozmiaru.

Modyfikatory

[edytuj]

volatile

[edytuj]

volatile znaczy ulotny. Oznacza to, że kompilator wyłączy dla takiej zmiennej optymalizacje typu zastąpienia przez stałą lub zawartość rejestru, za to wygeneruje kod, który będzie odwoływał się zawsze do komórek pamięci danego obiektu. Zapobiegnie to błędowi, gdy obiekt zostaje zmieniony przez część programu, która nie ma zauważalnego dla kompilatora związku z danym fragmentem kodu lub nawet przez zupełnie inny proces.

volatile float liczba1;
float liczba2;
{ 
 printf ("%f\n%f\n", liczba1, liczba2);
 /* instrukcje nie związane ze zmiennymi */ 
 printf ("%f\n%f", liczba1, liczba2);
}

Jeżeli zmienne liczba1 i liczba2 zmienią się niezauważalnie dla kompilatora to odczytując:

  • liczba1 - nastąpi odwołanie do komórek pamięci. Kompilator pobierze nową wartość zmiennej.
  • liczba2 - kompilator może wypisać poprzednią wartość, którą przechowywał w rejestrze.

Modyfikator volatile jest rzadko stosowany i przydaje się w wąskich zastosowaniach, jak współbieżność i współdzielenie zasobów oraz przerwania systemowe. Często jest stosowany przy tworzeniu programów na mikrokontrolery. Kompilatory często tak optymalizują kod, aby wszystkie operacje wykonywały się w rejestrach, przez co wartość zmiennej w pamięci może być przez dłuższy czas nieuaktualniana. Zastosowanie volatile zmusi kompilator do każdorazowego odwołania do pamięci w przypadku operowania na zmiennych.

register

[edytuj]

Jeżeli utworzymy zmienną, której będziemy używać w swoim programie bardzo często, możemy wykorzystać modyfikator register. Kompilator może wtedy umieścić zmienną w rejestrze, do którego ma szybki dostęp, co przyśpieszy odwołania do tej zmiennej

register int liczba;

W nowoczesnych kompilatorach ten modyfikator praktycznie nie ma wpływu na program. Optymalizator sam decyduje czy i co należy umieścić w rejestrze. Nie mamy żadnej gwarancji, że zmienna tak zadeklarowana rzeczywiście się tam znajdzie, chociaż dostęp do niej może zostać przyspieszony w inny sposób. Raczej powinno się unikać tego typu konstrukcji w programie.

static

[edytuj]

Pozwala na zdefiniowanie zmiennej statycznej. "Statyczność" polega na zachowaniu wartości pomiędzy kolejnymi definicjami tej samej zmiennej. Jest to przede wszystkim przydatne w funkcjach. Gdy zdefiniujemy zmienną w ciele funkcji, to zmienna ta będzie od nowa definiowana wraz z domyślną wartością (jeżeli taką podano). W wypadku zmiennej określonej jako statyczna, jej wartość się nie zmieni przy ponownym wywołaniu funkcji. Na przykład:

void dodaj(int liczba)
{
 int zmienna = 0;     /* bez static*/
 zmienna = zmienna + liczba;
 printf ("Wartosc zmiennej %d\n", zmienna);
}

Gdy wywołamy tę funkcję np. 3 razy w ten sposób:

 dodaj(3);
 dodaj(5);
 dodaj(4);

to ujrzymy na ekranie:

Wartosc zmiennej 3
Wartosc zmiennej 5
Wartosc zmiennej 4

jeżeli jednak deklarację zmiennej zmienimy na static int zmienna = 0, to wartość zmiennej zostanie zachowana i po ponownym wykonaniu funkcji powinnyśmy ujrzeć:

Wartosc zmiennej 3
Wartosc zmiennej 8
Wartosc zmiennej 12


Zupełnie co innego oznacza static zastosowane dla zmiennej globalnej. Jest ona wtedy widoczna tylko w jednym pliku. Zobacz też: rozdział Biblioteki.

extern

[edytuj]

Przez extern oznacza się zmienne globalne zadeklarowane w innych plikach - informujemy w ten sposób kompilator, żeby nie szukał jej w aktualnym pliku. Zobacz też: rozdział Biblioteki.

auto

[edytuj]

Zupełnym archaizmem jest modyfikator auto, który oznacza tyle, że zmienna jest lokalna. Ponieważ zmienna zadeklarowana w dowolnym bloku zawsze jest lokalna, modyfikator ten nie ma obecnie żadnego zastosowania praktycznego. auto jest spadkiem po wcześniejszych językach programowania, na których oparty jest C (np. B).

Konwersje typów

[edytuj]

Konwersje typów zmiennych :[32][33] [34]




Podstawowe operacje matematyczne

[edytuj]

Przypisanie

[edytuj]

Operator przypisania ("="), jak sama nazwa wskazuje, przypisuje wartość prawego argumentu lewemu, np.:

int a = 5, b;
b = a;
printf("%d\n", b); /* wypisze 5 */

Operator ten ma łączność prawostronną tzn. obliczanie przypisań następuje z prawa na lewo i zwraca on przypisaną wartość, dzięki czemu może być użyty kaskadowo:

int a, b, c;
a = b = c = 3;
printf("%d %d %d\n", a, b, c);  /* wypisze "3 3 3" */

Skrócony zapis

[edytuj]

C umożliwia też skrócony zapis postaci a #= b;, gdzie # jest jednym z operatorów: +, -, *, /, %, &, |, ^, << lub >> (opisanych niżej). Ogólnie rzecz ujmując zapis a #= b; jest równoważny zapisowi a = a # (b);, np.:

int a = 1;
a += 5;     /* to samo, co a = a + 5;       */
a /= a + 2; /* to samo, co a = a / (a + 2); */
a %= 2;     /* to samo, co a = a % 2;       */
d >>= 2; // Przesuń d w prawo * ang. right shift) o 2 pozycje i przypisz z powrotem do d

Rzutowanie

[edytuj]

Zadaniem rzutowania jest konwersja danej jednego typu na daną innego typu. Konwersja może być niejawna (domyślna konwersja przyjęta przez kompilator) lub jawna (podana explicite przez programistę). Oto kilka przykładów konwersji niejawnej:

int i = 42.7;            /* konwersja z double do int */
float f = i;             /* konwersja z int do float */
double d = f;            /* konwersja z float do double */
unsigned u = i;          /* konwersja z int do unsigned int */
f = 4.2;                 /* konwersja z double do float */
i = d;                   /* konwersja z double do int */
char *str = "foo";       /* konwersja z const char* do char*  [1] */
const char *cstr = str;  /* konwersja z char* do const char* */
void *ptr = str;         /* konwersja z char* do void* */

Podczas konwersji zmiennych zawierających większe ilości danych do typów prostszych (np. double do int) musimy liczyć się z utratą informacji, jak to miało miejsce w pierwszej linijce - zmienna int nie może przechowywać części ułamkowej toteż została ona odcięta i w rezultacie zmiennej została przypisana wartość 42.

Zaskakująca może się wydać linijka oznaczona przez [1]. Niejawna konwersja z typu const char* do typu char* nie jest dopuszczana przez standard C. Jednak literały napisowe (które są typu const char*) stanowią tutaj wyjątek. Wynika on z faktu, że były one używane na długo przed wprowadzeniem słówka const do języka i brak wspomnianego wyjątku spowodowałby, że duża część kodu zostałaby nagle zakwalifikowana jako niepoprawny kod.

Do jawnego wymuszenia konwersji służy jednoargumentowy operator rzutowania, np.:

double d = 3.14;
int pi = (int)d;         /* 1 */
pi = (unsigned)pi >> 4;  /* 2 */

W pierwszym przypadku operator został użyty, by zwrócić uwagę na utratę precyzji. W drugim, dlatego że bez niego operator przesunięcia bitowego zachowuje się trochę inaczej.

Obie konwersje przedstawione powyżej są dopuszczane przez standard jako jawne konwersje (tj. konwersja z double do int oraz z int do unsigned int), jednak niektóre konwersje są błędne, np.:

const char *cstr = "foo";
char *str = cstr;

W takich sytuacjach można użyć operatora rzutowania by wymusić konwersję:

const char *cstr = "foo";
char *str = (char*)cstr;

Należy unikać jednak takich sytuacji i nigdy nie stosować rzutowania by uciszyć kompilator. Zanim użyjemy operatora rzutowania należy się zastanowić co tak naprawdę będzie on robił i czy nie ma innego sposobu wykonania danej operacji, który nie wymagałby podejmowania tak drastycznych kroków.

Operatory arytmetyczne

[edytuj]

Język C definiuje następujące dwuargumentowe operatory arytmetyczne:

  • dodawanie ("+"),
  • odejmowanie ("-"),
  • mnożenie ("*"),
  • dzielenie ("/"),
  • reszta z dzielenia ("%") określona tylko dla liczb całkowitych (tzw. dzielenie modulo).


Dzielenie i mnożenie

[edytuj]
int a=7, b=2, c;
c = a % b;
printf ("%d\n",c); /* wypisze "1" */

Należy pamiętać, że (w pewnym uproszczeniu) wynik operacji jest typu takiego jak największy z argumentów. Oznacza to, że operacja wykonana na dwóch liczbach całkowitych nadal ma typ całkowity nawet jeżeli wynik przypiszemy do zmiennej rzeczywistej. Dla przykładu, poniższy kod:

float a = 7 / 2;
printf("%f\n", a);

wypisze (wbrew oczekiwaniu początkujących programistów) 3.0, a nie 3.5. Odnosi się to nie tylko do dzielenia, ale także mnożenia, np.:

float a = 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
printf("%f\n", a);

prawdopodobnie da o wiele mniejszy wynik niż byśmy się spodziewali. Aby wymusić obliczenia rzeczywiste należy zmienić typ jednego z argumentów na liczbę rzeczywistą po prostu zmieniając literał lub korzystając z rzutowania, np.:

float a = 7.0 / 2; /* wcześniejszy zapis: float a = 7 / 2; */ 
float b = (float)1000 * 1000 * 1000 * 1000 * 1000 * 1000;
printf("%f\n", a);
printf("%f\n", b);


Dzielenie

  • całkowite ( ang. integer division)[35][36]
  • zmiennoprzecinkowe ( ang. float division)

Zasady dzielenia liczb całkowitych i zmiennoprzecinkowych w C[37]

  • integer / integer = integer
  • float / integer = float
  • integer / float = float

Dodawanie i odejmowanie

[edytuj]

Operatory dodawania i odejmowania są określone również, gdy jednym z argumentów jest wskaźnik, a drugim liczba całkowita. Ten drugi jest także określony, gdy oba argumenty są wskaźnikami. O takim użyciu tych operatorów dowiesz się więcej w dalszej części książki.

Inkrementacja i dekrementacja

[edytuj]

Aby skrócić zapis wprowadzono dodatkowe operatory: inkrementacji ("++") i dekrementacji ("--"), które dodatkowo mogą być pre- lub postfiksowe. W rezultacie mamy więc cztery operatory:

Operatory inkrementacji zwiększa, a dekrementacji zmniejsza argument o jeden. Ponadto operatory pre- zwracają nową wartość argumentu, natomiast post- starą wartość argumentu.

 int a, b, c;
 a = 3;
 b = a--; /* po operacji b=3 a=2 */
 c = --b; /* po operacji b=2 c=2 */

Czasami (szczególnie w C++) użycie operatorów stawianych za argumentem jest nieco mniej efektywne (bo kompilator musi stworzyć nową zmienną by przechować wartość tymczasową).

Operatory bitowe

[edytuj]

Oprócz operacji znanych z lekcji matematyki w podstawówce, język C został wyposażony także w operatory bitowe[38][39][40][41], . Są to:

  • negacja bitowa (NOT)("~"),
  • koniunkcja bitowa (AND)("&"),
  • alternatywa bitowa (OR)("|") i
  • alternatywa rozłączna (XOR) ("^").


Działają one na poszczególnych bitach przez co mogą być szybsze od innych operacji.


Działanie tych operatorów można zdefiniować za pomocą poniższych tabel prawdy ( matryc logicznych):

W pierwszej tabeli a i b oznaczają bity ( albo pole bitowe o długości 1) , a nie liczby typu całkowitego

 "~" | a     "&" | a | b     "|" | a | b     "^" | a | b
-----+---   -----+---+---   -----+---+---   -----+---+---
  0  | 1      0  | 0 | 0      0  | 0 | 0      0  | 0 | 0
  1  | 0      1  | 1 | 1      1  | 1 | 1      0  | 1 | 1
              0  | 0 | 1      1  | 0 | 1      1  | 0 | 1
              0  | 1 | 0      1  | 1 | 0      1  | 1 | 0


W drugiej tabeli a i b oznaczają 4 bitowe pole ( c nie ma typu 4 bitowego)

   a   | 0101  =  5
   b   | 0011  =  3
-------+------
  ~a   | 1010  = 10
  ~b   | 1100  = 12
 a & b | 0001  =  1
 a | b | 0111  =  7
 a ^ b | 0110  =  6

Lub bardziej opisowo:

  • negacja bitowa daje w wyniku liczbę, która ma bity równe jeden tylko na tych pozycjach, na których argument miał bity równe zero;
  • koniunkcja bitowa daje w wyniku liczbę, która ma bity równe jeden tylko na tych pozycjach, na których oba argumenty miały bity równe jeden (mnemonik: 1 gdy wszystkie 1);
  • alternatywa bitowa daje w wyniku liczbę, która ma bity równe jeden na wszystkich tych pozycjach, na których jeden z argumentów miał bit równy jeden (mnemonik: 1 jeśli jest 1);
  • alternatywa rozłączna daje w wyniku liczbę, która ma bity równe jeden tylko na tych pozycjach, na których tylko jeden z argumentów miał bit równy jeden (mnemonik: 1 gdy różne).

Przy okazji warto zauważyć, że a ^ b ^ b to po prostu a. Właściwość ta została wykorzystana w różnych algorytmach szyfrowania oraz funkcjach haszujących. Alternatywę wyłączną stosuje się np. do szyfrowania kodu wirusów polimorficznych.


Negacja bitowa

[edytuj]

Jak wpływa długość liczby binarnej na wynik negacji ?

1 bit 4 bity 8 bitów
a
~a

Przykładowy program działajacy na liczbie 8 bitowej ( unsigned char ):

// gcc b.c -Wall
// ./a.out
#include <stdio.h>

/*     
https://stackoverflow.com/questions/699968/display-the-binary-representation-of-a-number-in-c
Chris Lutz
*/
void PrintBitsOfUChar(unsigned char v) {
  int i; // for C89 compatability
  for(i = 7; i >= 0; i--) putchar('0' + ((v >> i) & 1));
}



void TestBitwiseNot (unsigned char a ){

	printf ("decimal number  a = %3u;\t ", a );
	printf ("it's binary expansion = ");
	PrintBitsOfUChar(a);
	printf("\n"); 
	printf ("decimal number ~a = %3u;\t ", (unsigned char) ~a );
	printf ("it's binary expansion = ");
	PrintBitsOfUChar((unsigned char) ~a);
	printf("\n\n"); 
	
}






int main ()
{
 //unsigned char a;
 //char buffer[8];
 
 printf("unsigned char	has size = 1 byte = 8 bits and range from 0 to 255\n\n");
 
 TestBitwiseNot(0);
 TestBitwiseNot(255);
 TestBitwiseNot(5);
 TestBitwiseNot(3);
    
 
 return 0;
}


Wynik

unsigned char	has size = 1 byte = 8 bits and range from 0 to 255

decimal number  a =   0;	 it's binary expansion = 00000000
decimal number ~a = 255;	 it's binary expansion = 11111111

decimal number  a = 255;	 it's binary expansion = 11111111
decimal number ~a =   0;	 it's binary expansion = 00000000

decimal number  a =   5;	 it's binary expansion = 00000101
decimal number ~a = 250;	 it's binary expansion = 11111010

decimal number  a =   3;	 it's binary expansion = 00000011
decimal number ~a = 252;	 it's binary expansion = 11111100

Przesunięcie bitowe

[edytuj]

Dodatkowo, język C wyposażony jest w operatory przesunięcia bitowego w lewo ("<<") i prawo (">>"). Przesuwają one w danym kierunku bity lewego argumentu o liczbę pozycji podaną jako prawy argument.

  [variable]<<[numberOfPlaces]

Brzmi to może strasznie, ale wcale takie nie jest. Rozważmy operacje przesunięcia na liczbach 4-bitowych :

  a   | a<<1 | a<<2 | a>>1 | a>>2
------+------+------+------+------
 0001 | 0010 | 0100 | 0000 | 0000
 0011 | 0110 | 1100 | 0001 | 0000
 0101 | 1010 | 0100 | 0010 | 0001
 1000 | 0000 | 0000 | 0100 | 0010
 1111 | 1110 | 1100 | 0111 | 0011
 1001 | 0010 | 0100 | 0100 | 0010

Nie jest to zatem takie straszne na jakie wygląda.

Przesunięcie w lewą stronę oznacza przemieszczenie wszystkich bitów argumentu w lewo o określoną liczbę miejsc oraz wprowadzenie z prawej strony takiej samej ilości zer.

Przesunięcie w prawo oznacza przemieszczenie wszystkich bitów argumentu w prawo o określoną liczbę miejsc oraz powielenie najstarszego bitu na skrajnej lewej pozycji.[42]:

#include <stdio.h>

int main ()
{
 int a = 6;
 printf ("6 << 2 = %d\n", a<<2);  /* wypisze 24 */
 printf ("6 >> 2 = %d\n", a>>2);  /* wypisze 1 */
 return 0;
}

Inny przykład

#include <stdio.h>

int main() {
    unsigned int d = 0b11010110; // Binary: 11010110, Decimal: 214
    int k = 2;

    printf("Before shift: %u\n", d);

    d >>= k; // Right shift d by 2 positions and assign back to d

    printf("After shift: %u\n", d);

    return 0;
}


W tym przykładzie początkowa wartość d wynosi 214 (w formacie binarnym: 11010110).

Po przesunięciu w prawo o 2 pozycje (k wynosi 2) wynikiem jest 00110101, czyli 53 w systemie dziesiętnym.

Zatem wynikiem będzie:


Before shift: 214
After shift: 53

Zastosowania

[edytuj]

Operatorów bitowych używamy do:

  • operacji na zbiorach [43]
  • szyfrowania: XOR Encryption[44]
  • kompresji danych ( najmniejszy typ liczbowy ma 1 bajt = 8 bitów )[45]
  • szybkich obliczeń [46][47]
    • kod Graya [48]
    • obliczania liczb losowych
  • programowaniu systemów wbudowanych (ang. embedded system) [49]
  • programowania grafiki
    • OpenGl: pola bitowe w arumentach funkcji
  • sieciach komputerowych ( adres IP, Maska podsieci )
  • (a << b) jest równoważne pomnożeniu a przez 2^b (2 podniesione do potęgi b)[50]
  • (a>>b)‘ jest równoznaczne z podzieleniem a przez 2^b

Porównanie

[edytuj]

W języku C występują następujące operatory porównania:[51]

  • równe ("=="),
  • różne ("!="),
  • mniejsze ("<"),
  • większe (">"),
  • mniejsze lub równe ("<=") i
  • większe lub równe (">=").

Wykonują one odpowiednie porównanie swoich argumentów i zwracają jedynkę jeżeli warunek jest spełniony lub zero jeżeli nie jest.


równe

[edytuj]
// http://blogs.msdn.com/b/nativeconcurrency/archive/2012/10/11/floating-point-arithmetic-intricacies-in-c-amp.aspx

#include <stdio.h>

int main()
{if (0.0f == -0.0f)
    printf("equal\n");
    else printf("not equal ");

    return 0;
}

Częste błędy

[edytuj]

Porównajmy ze sobą dwa warunki:

(a = 1)
(a == 1)

Pierwszy z nich zawsze będzie prawdziwy, niezależnie od wartości zmiennej a! Dzieje się tak, ponieważ zostaje wykonane przypisanie do a wartości 1 a następnie jako wartość jest zwracane to, co zostało przypisane - czyli jeden. Drugi natomiast będzie prawdziwy tylko, gdy a jest równe 1.

W celu uniknięcia takich błędów niektórzy programiści zamiast pisać a == 1 piszą 1 == a, dzięki czemu pomyłka spowoduje, że kompilator zgłosi błąd.

Warto zauważyć, że kompilator GCC potrafi w pewnych sytuacjach wychwycić taki błąd. Aby zaczął to robić należy podać mu argument -Wparentheses.

Innym błędem jest użycie zwykłych operatorów porównania do sprawdzania relacji pomiędzy liczbami rzeczywistymi. Ponieważ operacje zmiennoprzecinkowe wykonywane są z pewnym przybliżeniem rzadko kiedy dwie zmienne typu float czy double są sobie równe. Dla przykładu:

#include <stdio.h>

int main ()
{
 float a, b, c;
 a = 1e10;   /* tj. 10 do potęgi 10 */
 b = 1e-10;  /* tj. 10 do potęgi -10 */
 c = b;      /* c = b */
 c = c + a;  /* c = b + a          (teoretycznie) */
 c = c - a;  /* c = b + a - a = b  (teoretycznie) */
 printf("%d\n", c == b); /* wypisze 0 */
}

Obejściem jest porównywanie modułu różnicy liczb. Również i takie błędy kompilator GCC potrafi wykrywać - aby to robił należy podać mu argument -Wfloat-equal.

Operatory logiczne

[edytuj]

Analogicznie do części operatorów bitowych, w C definiuje się operatory logiczne, mianowicie:

  • negację (zaprzeczenie): "!"
  • koniunkcję ("i"): "&&"
  • alternatywę ("lub"): "||"

Działają one bardzo podobnie do operatorów bitowych, jednak zamiast operować na poszczególnych bitach, biorą pod uwagę wartość logiczną argumentów.

"Prawda" i "fałsz" w języku C

[edytuj]

W języku C mamy 3 możliwości korzystania z "prawdy" i "fałszu":[52]

  • poprzez wartość wyrażenie : fałsz to zero a prawdy to wszystkie inne wartości
  • z użyciem dyrektywy preprocesora define : #define FALSE 0 ... #define TRUE !(FALSE)
  • użyciem typu bool


Typ bool

[edytuj]

Logiczny typ danych[53] ( ang. Boolean data type ) składa się z dokładnie dwóch elementów: prawdy (ang. true, 1, +) i fałszu (ang. false, 0, -).

Typu bool możemy używać na 4 sposoby : [54]

  • w C99 ( nie w C90 [55]) poprzez dodanie dyrektywy #include <stdbool.h>
  • trzy sposoby z użyciem dyrektyw prepocesora :
typedef int bool;
#define true 1
#define false 0
typedef int bool;
enum { false, true };
typedef enum { false, true } bool;

Wartość wyrażenia

[edytuj]

W tym przypadku nie używamy specjalnego typu danych do operacji logicznych. Operatory logiczne można stosować do liczb (np. typu int), tak samo jak operatory bitowe albo arytmetyczne.

Wyrażenie ma wartość logiczną:

  • 0, czyli jest "fałszywe" wtedy i tylko wtedy, gdy jest równe 0
  • 1 czyli jest "prawdziwe", gdy jest różne od zera

Operatory logiczne w wyniku dają zawsze albo 0 albo 1.

Żeby w pełni uzmysłowić sobie, co to to oznacza, spójrzmy na wynik wykonania poniższych trzech linijek:

printf("koniunkcja: %d\n", 18 && 19);
printf("alternatywa: %d\n", 'a' || 'b');
printf("negacja: %d\n", !20);
koniunkcja: 1
alternatywa: 1
negacja: 0

Liczba 18 nie jest równa 0, więc ma wartość logiczną 1. Podobnie 19 ma wartość logiczną 1. Dlatego ich koniunkcja jest równa 1. Znaki 'a' i 'b' zostaną w wyrażeniu logicznym potraktowane jako liczby o wartości odpowiadającej kodowi ASCII znaku — czyli oba będą miały wartość logiczną 1. Liczba 20 również ma wartość logiczną 1 (bo nie jest zerem), dlatego jej negacja to 0 czyli fałsz.

Dalsze przykłady w dziale o instrukcjach sterujących.

Skrócone obliczanie wyrażeń logicznych

[edytuj]

Język C wykonuje skrócone obliczanie wyrażeń logicznych - to znaczy, oblicza wyrażenie tylko tak długo, jak nie wie, jaka będzie jego ostateczna wartość.[56] To znaczy, idzie od lewej do prawej obliczając kolejne wyrażenia (dodatkowo na kolejność wpływ mają nawiasy) i gdy będzie miał na tyle informacji, by obliczyć wartość całości, nie liczy reszty. Może to wydawać się niejasne, ale przyjrzyjmy się wyrażeniom logicznym:

A && B
A || B

Jeśli A jest fałszywe, nie trzeba obliczać B w pierwszym wyrażeniu, bo koniunkcja fałszu i dowolnego wyrażenia zawsze da fałsz. Analogicznie, w drugim przykładzie, jeśli A jest prawdziwe, to całe wyrażenie jest prawdziwe i wartość B nie ma znaczenia.

Poza zwiększoną szybkością zysk z takiego rozwiązania polega na możliwości stosowania efektów ubocznych. Idea efektu ubocznego opiera się na tym, że w wyrażeniu można wywołać funkcje, które będą robiły poza zwracaniem wyniku inne rzeczy, oraz używać podstawień. Popatrzmy na poniższy przykład:

( (a > 0) || (a < 0) || (a = 1) )

Jeśli a będzie większe od 0 to obliczona zostanie tylko wartość wyrażenia (a > 0) - da ono prawdę, czyli reszta obliczeń nie będzie potrzebna. Jeśli a będzie mniejsze od zera, najpierw zostanie obliczone pierwsze podwyrażenie a następnie drugie, które da prawdę. Ciekawy będzie jednak przypadek, gdy a będzie równe zero - do a zostanie wtedy podstawiona jedynka i całość wyrażenia zwróci prawdę (bo 1 jest traktowane jak prawda).

Efekty uboczne pozwalają na różne szaleństwa i wykonywanie złożonych operacji w samych warunkach logicznych, jednak przesadne używanie tego typu konstrukcji powoduje, że kod staje się nieczytelny i jest uważane za zły styl programistyczny.

Operator wyrażenia warunkowego

[edytuj]

C posiada szczególny rodzaj operatora - to operator ?: zwany też operatorem wyrażenia warunkowego. Jest to jedyny operator w tym języku przyjmujący trzy argumenty.

a ? b : c

Jego działanie wygląda następująco: najpierw oceniana jest wartość logiczna wyrażenia a; jeśli jest ono prawdziwe, to zwracana jest wartość b, jeśli natomiast wyrażenie a jest nieprawdziwe, zwracana jest wartość c.

Praktyczne zastosowanie - znajdowanie większej z dwóch liczb:

a = (b>=c) ? b : c;     /* Jeśli b jest większe bądź równe c, to zwróć b. 
                           W przeciwnym wypadku zwróć c. */

lub zwracanie modułu liczby:

a = a < 0 ? -a : a;

Wartości wyrażeń są przy tym operatorze obliczane tylko jeżeli zachodzi taka potrzeba, np. w wyrażeniu 1 ? 1 : foo() funkcja foo() nie zostanie wywołana.

Operator przecinek

[edytuj]

Operator przecinek jest dość dziwnym operatorem. Powoduje on obliczanie wartości wyrażeń od lewej do prawej po czym zwrócenie wartości ostatniego wyrażenia.[57] W zasadzie, w normalnym kodzie programu ma on niewielkie zastosowanie, gdyż zamiast niego lepiej rozdzielać instrukcje zwykłymi średnikami. Ma on jednak zastosowanie w instrukcji sterującej for.

Operator sizeof

[edytuj]

Operator sizeof zwraca rozmiar w bajtach (gdzie bajtem jest zmienna typu char) podanego typu lub typu podanego wyrażenia. Ma on dwa rodzaje: sizeof(typ) lub sizeof wyrażenie. Przykładowo:

#include <stdio.h>

int main()
{
 printf(" sizeof(short)=%lu\n sizeof(int)=%lu\n sizeof(long)=%lu\n", sizeof(short), sizeof(int), sizeof(long));
 return 0;
}

Operator ten jest często wykorzystywany przy dynamicznej alokacji pamięci, co zostanie opisane w rozdziale poświęconym wskaźnikom.

Pomimo, że w swej budowie operator sizeof bardzo przypomina funkcję, to jednak nią nie jest. Wynika to z trudności w implementacji takowej funkcji - jej specyfika musiałaby odnosić się bezpośrednio do kompilatora. Ponadto jej argumentem musiałyby być typy, a nie zmienne. W języku C nie jest możliwe przekazywanie typu jako argumentu. Ponadto często zdarza się, że rozmiar zmiennej musi być wiadomy jeszcze w czasie kompilacji - to ewidentnie wyklucza implementację sizeof() jako funkcji.

Wynik operatora sizeof jest typu size_t

Inne operatory

[edytuj]

Poza wyżej opisanymi operatorami istnieją jeszcze:

  • operator "[]" opisany przy okazji opisywania tablic;
  • jednoargumentowe operatory "*" i "&" opisane przy okazji opisywania wskaźników;
  • operatory "." i "->" opisywane przy okazji opisywania struktur i unii;
  • operator "()" będący operatorem wywołania funkcji,
  • operator "()" grupujący wyrażenia (np. w celu zmiany kolejności obliczania)

Wyrażenie

[edytuj]

Priorytety i kolejność obliczeń

[edytuj]

Jak w matematyce, również i w języku C obowiązuje pewna ustalona kolejność działań. Aby móc ją określić należy ustalić dwa parametry danego operatora: jego priorytet oraz łączność. Przykładowo operator mnożenia ma wyższy priorytet niż operator dodawania i z tego powodu w wyrażeniu najpierw wykonuje się mnożenie, a dopiero potem dodawanie.

Drugim parametrem jest łączność - określa ona od której strony wykonywane są działania w przypadku połączenia operatorów o tym samym priorytecie. Na przykład odejmowanie ma łączność lewostronną i da w wyniku -2. Gdyby miało łączność prawostronną, wynikiem byłoby 2. Przykładem matematycznego operatora, który ma łączność prawostronną jest potęgowanie, np. jest równe (łączność lewostronna dałaby wynik ).

W języku C występuje dużo poziomów operatorów. Poniżej przedstawiamy tabelkę ze wszystkimi operatorami poczynając od tych z najwyższym priorytetem (wykonywanych na początku).

Operator Łączność
nawiasy nie dotyczy
jednoargumentowe przyrostkowe: [] . -> wywołanie funkcji postinkrementacja postdekrementacja lewostronna
jednoargumentowe przedrostkowe: ! ~ + - * & sizeof preinkrementacja predekrementacja rzutowanie prawostronna
* / % lewostronna
+ - lewostronna
<< >> lewostronna
< <= > >= lewostronna
== != lewostronna
& lewostronna
^ lewostronna
| lewostronna
&& lewostronna
|| lewostronna
?: prawostronna
operatory przypisania prawostronna
, lewostronna

Duża liczba poziomów pozwala czasami zaoszczędzić trochę milisekund w trakcie pisania programu i bajtów na dysku, gdyż często nawiasy nie są potrzebne, nie należy jednak z tym przesadzać, gdyż kod programu może stać się mylący nie tylko dla innych, ale po latach (czy nawet i dniach) również dla nas.

Warto także podkreślić, że operator koniunkcji ma niższy priorytet niż operator porównania[58]. Oznacza to, że kod

if (flags & FL_MASK == FL_FOO)

zazwyczaj da rezultat inny od oczekiwanego. Najpierw bowiem wykona się porównanie wartości FL_MASK z wartością FL_FOO, a dopiero potem koniunkcja bitowa. W takich sytuacjach należy pamiętać o użyciu nawiasów:

if ((flags & FL_MASK) == FL_FOO)

Kompilator GCC potrafi wykrywać takie błędy i aby to robił należy podać mu argument -Wparentheses.

Kolejność wyliczania argumentów operatora

[edytuj]

W przypadku większości operatorów (wyjątkami są tu &&, || i przecinek) nie da się określić, która wartość argumentu zostanie obliczona najpierw. W większości przypadków nie ma to większego znaczenia, lecz w przypadku wyrażeń, które mają efekty uboczne, wymuszenie konkretnej kolejności może być potrzebne. Weźmy dla przykładu program

#include <stdio.h>

int foo(int a) 
{
 printf("%d\n", a);
 return 0;
}
 
int main(void) 
{
 return foo(1) + foo(2);
}

Otóż nie wiemy czy najpierw zostanie wywołana funkcja foo z parametrem jeden, czy dwa. Jeżeli ma to znaczenie należy użyć zmiennych pomocniczych, zmieniając definicję funkcji main na:

int main(void) 
{
 int tmp = foo(1);
 return tmp + foo(2);
}

Teraz już na pewno najpierw zostanie wypisana jedynka, a potem dopiero dwójka. Sytuacja jeszcze bardziej się komplikuje, gdy używamy wyrażeń z efektami ubocznymi jako argumentów funkcji, np.:

#include <stdio.h>

int foo(int a) 
{
 printf("%d\n", a);
 return 0;
}
 
int bar(int a, int b, int c, int d) 
{
 return a + b + c + d;
}
 
int main(void) 
{
 return bar(foo(1), foo(2), foo(3), foo(4));
}

Teraz też nie wiemy, która z 24 permutacji liczb 1, 2, 3 i 4 zostanie wypisana i ponownie należy pomóc sobie zmiennymi tymczasowymi, jeżeli zależy nam na konkretnej kolejności:

int main(void) 
{
 int tmp1 = foo(1);
 int tmp2 = foo(2);
 int tmp3 = foo(3);
 return bar(tmp1, tmp2, tmp3, foo(4));
}

Jak czytać wyrażenia?

[edytuj]


Reguła spiralna wg Davida Anderson[60], która umożliwia każdemu programiście C przeanalizowanie dowolnej deklaracji C

  • Zacznij od nieznanego elementu : opisz go
  • poruszaj się spiralnie, zgodnie z ruchem wskazówek zegara
  • spotykając kolejne elementy, zastąp je odpowiednim opisem
  • Kontynuuj to w kierunku spiralnym/zgodnym z ruchem wskazówek zegara, aż wszystkie elementy zostaną opisane
  • Zawsze najpierw rozwiązuj wszystko, co jest w nawiasach!

Przykład 1

[edytuj]
char *str[10]

Co to jest str ?

Zaczynamy od środka :

str jest to 

Poruszamy się spiralnie w kierunku zgodnym z ruchem wskazówek zegara, zaczynając od `str', a pierwszym znakiem, który widzimy, jest `[', co oznacza, że mamy tablicę, więc...

str to tablica zawierająca 10 elementów

Kontynuuj w kierunku spiralnym, i dochodzimy do "char *"

 str to tablica zawierająca 10 wskaźników na char
 

Odpowiedź z cdcl :

declare str as array 10 of pointer to char


                     +-------+
                     | +-+   |
                     | ^ |   |
                char *str[10];
                 ^   ^   |   |
                 |   +---+   |
                 +-----------+

Przykład 2

                     +--------------------+
                     | +---+              |
                     | |+-+|              |
                     | |^ ||              |
                char *(*fp)( int, float *);
                 ^   ^ ^  ||              |
                 |   | +--+|              |
                 |   +-----+              |
                 +------------------------+

Przykład 3

                      +-----------------------------+
                      |                  +---+      |
                      |  +---+           |+-+|      |
                      |  ^   |           |^ ||      |
                void (*signal(int, void (*fp)(int)))(int);
                 ^    ^      |      ^    ^  ||      |
                 |    +------+      |    +--+|      |
                 |                  +--------+      |
                 +----------------------------------+

Uwagi

[edytuj]
  • W języku C++ wprowadzony został dodatkowo inny sposób zapisu rzutowania, który pozwala na łatwiejsze znalezienie w kodzie miejsc, w których dokonujemy rzutowania. Więcej na stronie C++/Zmienne.

Zobacz też

[edytuj]




Instrukcje sterujące

[edytuj]

C jest językiem imperatywnym - oznacza to, że instrukcje wykonują się jedna po drugiej w takiej kolejności w jakiej są napisane. Aby móc zmienić kolejność wykonywania instrukcji potrzebne są instrukcje sterujące.

Na wstępie przypomnijmy jeszcze informację z rozdziału Operatory, że wyrażenie jest prawdziwe wtedy i tylko wtedy, gdy jest różne od zera, a fałszywe wtedy i tylko wtedy, gdy jest równe zeru.



Instrukcje warunkowe

[edytuj]

if

[edytuj]

Użycie instrukcji if wygląda tak:

 if (wyrażenie) {
   /* blok wykonany, jeśli wyrażenie jest prawdziwe */
 }
 /* dalsze instrukcje */

Istnieje także możliwość reakcji na nieprawdziwość wyrażenia - wtedy należy zastosować słowo kluczowe else:

 if (wyrażenie) {
   /* blok wykonany, jeśli wyrażenie jest prawdziwe */
 } else {
   /* blok wykonany, jeśli wyrażenie jest nieprawdziwe */
 }
 /* dalsze instrukcje */

Przypatrzmy się bardziej "życiowemu" programowi, który porównuje ze sobą dwie liczby:

 #include <stdio.h>
 
 int main ()
 {
   int a, b;
   a = 4;
   b = 6;
   if (a==b) {
     printf ("a jest równe b\n");
   } else {
     printf ("a nie jest równe b\n");
   }
   return 0;
 }

Stosowany jest też krótszy zapis warunków logicznych, korzystający z tego, jak C rozumie prawdę i fałsz, tzn.:

  • liczba całkowita różna od zera oznacza prawdę
  • liczba całkowita równa zero oznacza fałsz.

Jeśli zmienna a jest typu integer, zamiast:

 if (a != 0) b = 1/a;

można napisać:

  if (a) b = 1/a;

a zamiast

  if (a == 0) b = 0;

można napisać:

  if (!a)  b = 0;


Czasami zamiast pisać instrukcję if możemy użyć operatora wyrażenia warunkowego (patrz Operatory).

 if (a != 0)
   b = 1/a;
 else
   b = 0;

ma dokładnie taki sam efekt jak:

 b = (a !=0) ? 1/a : 0;

Zobacz też:

switch

[edytuj]

Aby ograniczyć wielokrotne stosowanie instrukcji if możemy użyć switch. Jej użycie wygląda tak:

 switch (wyrażenie) {
   case wartość1: /* instrukcje, jeśli wyrażenie == wartość1 */
     break;
   case wartość2: /* instrukcje, jeśli wyrażenie == wartość2 */
     break;
   /* ... */
   default: /* instrukcje, jeśli żaden z wcześniejszych warunków nie został spełniony */
     break;
 }

Należy pamiętać o użyciu break po zakończeniu listy instrukcji następujących po case. Jeśli tego nie zrobimy, program przejdzie do wykonywania instrukcji z następnego case. Może mieć to fatalne skutki:

 #include <stdio.h>
 
 int main ()
 {
   int a, b;
   printf ("Podaj a: ");
   scanf ("%d", &a);
   printf ("Podaj b: ");
   scanf ("%d", &b);
   switch (b) {
     case  0: printf ("Nie można dzielić przez 0!\n"); /* tutaj zabrakło break! */
     default: printf ("a/b=%d\n", a/b);
   }
   return 0;
 }

A czasami może być celowym zabiegiem (tzw. "fall-through") - wówczas warto zaznaczyć to w komentarzu. Oto przykład:

 #include <stdio.h>
 
 int main ()
 {
   int a = 4;
   switch ((a%3)) {
     case  0:
       printf ("Liczba %d dzieli się przez 3\n", a);
       break;
     case -2:
     case -1:
     case  1:
     case  2:
       printf ("Liczba %d nie dzieli się przez 3\n", a);
       break;
   }
   return 0;
 }

Przeanalizujmy teraz działający przykład:

 #include <stdio.h>
 
 int main ()
 {
   unsigned int dzieci = 3, podatek=1000;
   switch (dzieci) {
      case  0: break; /* brak dzieci - czyli brak ulgi */ 
      case  1: /* ulga  2% */
        podatek = podatek - (podatek/100* 2); 
        break;
      case  2: /* ulga  5% */
        podatek = podatek - (podatek/100* 5);
        break;
      default: /* ulga 10% */
        podatek = podatek - (podatek/100*10);
        break; 
   }
   printf ("Do zapłaty: %d\n", podatek);
 }

Pętle

[edytuj]

while

[edytuj]

Często zdarza się, że nasz program musi wielokrotnie powtarzać ten sam ciąg instrukcji. Aby nie przepisywać wiele razy tego samego kodu można skorzystać z tzw. pętli. Pętla wykonuje się dopóty, dopóki prawdziwy jest warunek.

 while (warunek) {
   /* instrukcje do wykonania w pętli */
 }
 /* dalsze instrukcje */

Całą zasadę pętli zrozumiemy lepiej na jakimś działającym przykładzie. Załóżmy, że mamy obliczyć kwadraty liczb od 1 do 10. Piszemy zatem program:

 #include <stdio.h>
 
 int main ()
 {
   int a = 1;
   while (a <= 10) { /* dopóki a nie przekracza 10 */
     printf ("%d\n", a*a); /* wypisz a*a na ekran*/
     ++a; /* zwiększamy a o jeden*/
   }
   return 0;
 }

Po analizie kodu mogą nasunąć się dwa pytania:

  • Po co zwiększać wartość a o jeden? Otóż gdybyśmy nie dodali instrukcji zwiększającej a, to warunek zawsze byłby spełniony, a pętla "kręciłaby" się w nieskończoność.
  • Dlaczego warunek to "a <= 10" a nie "a!=10"? Odpowiedź jest dość prosta. Pętla sprawdza warunek przed wykonaniem kolejnego "obrotu". Dlatego też gdyby warunek brzmiał "a!=10" to dla a=10 jest on nieprawdziwy i pętla nie wykonałaby ostatniej iteracji, przez co program generowałby kwadraty liczb od 1 do 9, a nie do 10.

for

[edytuj]

Od instrukcji while czasami wygodniejsza jest instrukcja for. Umożliwia ona wpisanie ustawiania zmiennej, sprawdzania warunku i inkrementowania zmiennej w jednej linijce co często zwiększa czytelność kodu.

Instrukcję for stosuje się w następujący sposób:

 for (wyrażenie1; wyrażenie2; wyrażenie3) {
   /* instrukcje do wykonania w pętli */
 }
 /* dalsze instrukcje */

Jak widać, pętla for znacznie różni się od tego typu pętli, znanych w innych językach programowania.

Opiszemy więc, co oznaczają poszczególne wyrażenia:

  • wyrażenie1 (ang. initializationStatement) - jest to instrukcja, która będzie wykonana przed pierwszym przebiegiem pętli. Zwykle jest to inicjalizacja zmiennej, która będzie służyła jako "licznik" przebiegów pętli.
  • wyrażenie2 (ang. testExpression) - jest warunkiem trwania pętli. Pętla wykonuje się tak długo, jak prawdziwy jest ten warunek.
  • wyrażenie3 (ang. updateStatement) - jest to instrukcja, która wykonywana będzie po każdym przejściu pętli ( także po ostatnim). Zamieszczone są tu instrukcje, które zwiększają licznik o odpowiednią wartość.

Jeżeli wewnątrz pętli nie ma żadnych instrukcji continue (opisanych niżej) to jest ona równoważna z:

 {
   wyrażenie1;
   while (wyrażenie2) {
     /* instrukcje do wykonania w pętli */
     wyrażenie3;
   }
 }
 /* dalsze instrukcje */

Ważną rzeczą jest tutaj to, żeby zrozumieć i zapamiętać jak tak naprawdę działa pętla for. Początkującym programistom nieznajomość tego faktu sprawia wiele problemów.

W pierwszej kolejności w pętli for wykonuje się wyrażenie1. Wykonuje się ono zawsze, nawet jeżeli warunek przebiegu pętli jest od samego początku fałszywy.

Po wykonaniu wyrażenie1 pętla for sprawdza warunek zawarty w wyrażenie2, jeżeli jest on prawdziwy ( inny niż zero), to wykonywana jest treść pętli for, czyli najczęściej to co znajduje się między klamrami, lub gdy ich nie ma, następna pojedyncza instrukcja. W szczególności musimy pamiętać, że sam średnik też jest instrukcją - instrukcją pustą.

Gdy już zostanie wykonana treść pętli for, następuje wykonanie wyrażenie3. Należy zapamiętać, że wyrażenie3 zostanie wykonane, nawet jeżeli był to już ostatni obieg pętli. Poniższe 4 przykłady pętli for w rezultacie dadzą ten sam wynik. Wypiszą na ekran liczby od 1 do 10.


 
 for(i=1; i<=10; ++i){
  printf("%d", i);
 }

 for(i=1; i<=10; ++i){
  printf("%d", i);
}

 for(i=1; i<=10; printf("%d", i++ ) );

 
 // 4 przykład 
 for(i=1; i<10; printf("i = %d", i++ ) ); 
 printf(" i = %d", i ); // wyrażenie3 i++ zostanie wykonane, nawet jeżeli był to już ostatni obieg pętli

Dwa pierwsze przykłady korzystają z własności struktury blokowej, kolejny przykład jest już bardziej wyrafinowany i korzysta z tego, że jako wyrażenie3 może zostać podane dowolne bardziej skomplikowane wyrażenie, zawierające w sobie inne podwyrażenia. A oto kolejny program, który najpierw wyświetla liczby w kolejności rosnącej, a następnie wraca.

 #include <stdio.h>
 int main()
 {
  int i;
  for(i=1; i<=5; ++i){   
    printf("%d", i);
    }  

  for( ; i>=1; --i){
    printf("%d", i);
    }
 
  return 0;
 }

Po analizie powyższego kodu, początkujący programista może stwierdzić, że pętla wypisze 123454321. Stanie się natomiast inaczej. Wynikiem działania powyższego programu będzie ciąg cyfr 12345654321. Pierwsza pętla wypisze cyfry "12345", lecz po ostatnim swoim obiegu pętla for (tak jak zwykle) zinkrementuje zmienną i. Gdy druga pętla przystąpi do pracy, zacznie ona odliczać począwszy od liczby i=6, a nie 5. By spowodować wyświetlanie liczb od 1 do 5 i z powrotem wystarczy gdzieś między ostatnim obiegiem pierwszej pętli for a pierwszym obiegiem drugiej pętli for zmniejszyć wartość zmiennej i o 1.

Niech podsumowaniem będzie jakiś działający fragment kodu, który może obliczać wartości kwadratów liczb od 1 do 10.

   
 #include <stdio.h>
 
 int main ()
 {
   int a;
   for (a=1; a<=10; ++a) {
     printf ("%d\n", a*a);
   }
   return 0;
 }

do..while

[edytuj]

Pętle while i for mają jeden zasadniczy mankament - może się zdarzyć, że nie wykonają się ani razu. Aby mieć pewność, że nasza pętla będzie miała co najmniej jeden przebieg musimy zastosować pętlę do while. Wygląda ona następująco:

 do {
   /* instrukcje do wykonania w pętli */
 } while (warunek);
 /* dalsze instrukcje */

Zasadniczą różnicą pętli do while jest fakt, iż sprawdza ona warunek pod koniec swojego przebiegu. To właśnie ta cecha decyduje o tym, że pętla wykona się co najmniej raz. A teraz przykład działającego kodu, który tym razem będzie obliczał trzecią potęgę liczb od 1 do 10.

 #include <stdio.h>
 
 int main ()
 {
   int a = 1;
   do {
     printf ("%d\n", a*a*a);
     ++a;
   } while (a <= 10);
   return 0;
 }

Może się to wydać zaskakujące, ale również przy tej pętli zamiast bloku instrukcji można zastosować pojedynczą instrukcję, np.:

 #include <stdio.h>
 
 int main ()
 {
   int a = 1;
   do printf ("%d\n", a*a*a); while (++a <= 10);
   return 0;
 }

break

[edytuj]

Instrukcja break pozwala na opuszczenie wykonywania pętli w dowolnym momencie. Przykład użycia:

 int a;
 for (a=1 ; a != 9 ; ++a) {
   if (a == 5) break;
   printf ("%d\n", a);
 }

Program wykona tylko 4 przebiegi pętli, gdyż przy 5 przebiegu instrukcja break spowoduje wyjście z pętli.

Break i pętle nieskończone

[edytuj]

W przypadku pętli for nie trzeba podawać warunku. W takim przypadku kompilator przyjmie, że warunek jest stale spełniony. Oznacza to, że poniższe pętle są równoważne:

 for (;;) { /* ... */ }
 for (;1;) { /* ... */ }
 for (a;a;a) { /* ... */} /*gdzie a jest dowolną liczba rzeczywistą różną od 0*/
 while (1) { /* ... */ }
 do { /* ... */ } while (1);

Takie pętle nazywamy pętlami nieskończonymi, które przerwać może jedynie instrukcja break[61](z racji tego, że warunek pętli zawsze jest prawdziwy) [62].

Wszystkie fragmenty kodu działają identycznie:

 int i = 0;
 for (;i!=5;++i) {
   /* kod ... */
 }

 int i = 0;
 for (;;++i) {
   if (i == 5) break;
 }

 int i = 0;
 for (;;) {
   if (i == 5) break;
   ++i;
 }

continue

[edytuj]

W przeciwieństwie do break, która przerywa wykonywanie pętli instrukcja continue powoduje przejście do następnej iteracji, o ile tylko warunek pętli jest spełniony. Przykład:

 int i;
 for (i = 0 ; i < 100 ; ++i) {
   printf ("Poczatek\n");
   if (i > 40) continue ;
   printf ("Koniec\n");
 }

Dla wartości i większej od 40 nie będzie wyświetlany komunikat "Koniec". Pętla wykona pełne 100 przejść.


Oto praktyczny przykład użycia tej instrukcji:

 #include <stdio.h>
 int main()
 {
   int i;
   for (i = 1 ; i <= 50 ; ++i) {
     if (i%4 == 0) continue ;
     printf ("%d, ", i);
   }
   return 0;
 }

Powyższy program generuje liczby z zakresu od 1 do 50, które nie są podzielne przez 4.

goto

[edytuj]

Istnieje także instrukcja, która dokonuje skoku do dowolnego miejsca programu, oznaczonego tzw. etykietą.

 etykieta:
 /* instrukcje */
 goto etykieta;

Uwaga!: kompilator GCC w wersji 4.0 i wyższych jest bardzo uczulony na etykiety zamieszczone przed nawiasem klamrowym, zamykającym blok instrukcji. Innymi słowy: niedopuszczalne jest umieszczanie etykiety zaraz przed klamrą, która kończy blok instrukcji, zawartych np. w pętli for. Można natomiast stosować etykietę przed klamrą kończącą daną funkcję.

Przykład uzasadnionego użycia:

 int i,j;
 for (i = 0; i < 10; ++i) {
   for (j = i; j < i+10; ++j) {
     if (i + j % 21 == 0) goto koniec;
   }
 }
 koniec:
 /* dalsza czesc programu */

Zobacz również : obsługa nielokalnych skoków

Natychmiastowe kończenie programu - funkcja exit

[edytuj]

Program może zostać w każdej chwili zakończony - do tego właśnie celu służy funkcja exit. Używamy jej następująco:

 exit (kod_wyjścia);

Liczba całkowita kod_wyjścia jest przekazywana do procesu macierzystego, dzięki czemu dostaje on informację, czy program w którym wywołaliśmy tą funkcję zakończył się poprawnie lub czy się tak nie stało. Kody wyjścia są nieustandaryzowane i żeby program był w pełni przenośny należy stosować makra EXIT_SUCCESS i EXIT_FAILURE, choć na wielu systemach kod 0 oznacza poprawne zakończenie, a kod różny od 0 błędne. W każdym przypadku, jeżeli nasz program potrafi generować wiele różnych kodów, warto je wszystkie udokumentować w ew. dokumentacji. Są one też czasem pomocne przy wykrywaniu błędów.

Odwijanie pętli

[edytuj]

Odwijanie pętli ( ang. Loop unrolling) jest metodą optymalizacji oprogramowania powodującą przyspieszenie wykonania pętli. Polega na zmianie kodu programu przez kilkukrotne skopiowanie zawartości pętli i odpowiednie zmniejszenie liczby powtórzeń. Dzięki temu eliminuje się niepotrzebne sprawdzanie warunku zakończenia.

Przykładowo pętla wykonująca 100 razy funkcję delete(x), również 100 razy sprawdzi warunek zakończenia x<100:

 for (int x = 0; x < 100; x++)
 {
     delete(x);
 }

Można jednak nieznacznie wydłużyć kod powtarzając instrukcję delete(x) oraz zmniejszyć liczbę przejść przez pętlę, przez co wykona się kilkukrotnie mniej sprawdzeń x<100:

 for (int x = 0; x < 100; x += 5)
 {
     delete(x);
     delete(x+1);
     delete(x+2);
     delete(x+3);
     delete(x+4);
 }

Użycie odwijania pętli zwiększa objętość programu, dlatego potrzebne jest znalezienie optymalnej liczby powtórzeń. Stosowanie odwijania pętli w programowaniu nie jest jednak konieczne ponieważ większość kompilatorów sama znajduje optymalną wersję odwinięcia i umieszcza ją w kodzie wynikowym.

Uwagi

[edytuj]
  • W języku C++ można deklarować zmienne w nagłówku pętli "for" w następujący sposób: for(int i=0; i<10; ++i) (więcej informacji w C++/Zmienne)
  • [C/Operatory#Operator_wyrażenia_warunkowego| Operator_wyrażenia_warunkowego ?]



Podstawowe procedury wejścia i wyjścia

[edytuj]

Komputer byłby całkowicie bezużyteczny, gdyby użytkownik nie mógł się z nim porozumieć (tj. wprowadzić danych lub otrzymać wyników pracy programu). Programy komputerowe służą w największym uproszczeniu do obróbki danych - więc muszą te dane jakoś od nas otrzymać, przetworzyć i przekazać nam wynik.

Takie wczytywanie i "wyrzucanie" danych w terminologii komputerowej nazywamy wejściem (input) i wyjściem (output). Bardzo często mówi się o wejściu i wyjściu danych łącznie - input/output, albo po prostu I/O.

W C do komunikacji z użytkownikiem służą odpowiednie funkcje, które możemy znależć w standardowej bibliotece stdio ( plik stdio.h) . Zresztą, do wielu zadań w C służą funkcje. Używając funkcji, nie musimy wiedzieć, w jaki sposób komputer wykonuje jakieś zadanie, interesuje nas tylko to, co ta funkcja robi. Funkcje niejako "wykonują za nas część pracy", ponieważ nie musimy pisać być może dziesiątek linijek kodu, żeby np. wypisać tekst na ekranie (wbrew pozorom - kod funkcji wyświetlającej tekst na ekranie jest dość skomplikowany). Jeszcze taka uwaga - gdy piszemy o jakiejś funkcji, zazwyczaj podając jej nazwę dopisujemy na końcu nawias:

printf()
scanf()

żeby było jasne, że chodzi o funkcję, a nie o coś innego.

Wyżej wymienione funkcje to jedne z najczęściej używanych funkcji w C - pierwsza służy do wypisywania danych na ekran, natomiast druga do wczytywania danych z klawiatury. W zasadzie standard C nie definiuje czegoś takiego jak ekran i klawiatura - mowa w nim o standardowym wyjściu i standardowym wejściu. Zazwyczaj jest to właśnie ekran i klawiatura, ale nie zawsze. W szczególności użytkownicy Linuksa lub innych systemów uniksowych mogą być przyzwyczajeniu do przekierowania wejścia/wyjścia z/do pliku czy łączenie komend w potoki (ang. pipe). W takich sytuacjach dane nie są wyświetlane na ekranie, ani odczytywane z klawiatury.


Zanim będzie można odczytywać lub zapisywać zawartość pliku, należy ustanowić połączenie lub kanał komunikacji z plikiem. Ten proces nazywa się otwieraniem pliku. Możesz otworzyć plik do odczytu, zapisu lub obu. Połączenie z otwartym plikiem jest reprezentowane jako strumień lub deskryptor pliku. Przekazujesz to jako argument do funkcji, które wykonują rzeczywiste operacje odczytu lub zapisu, aby powiedzieć im, na którym pliku mają działać. Niektóre funkcje oczekują strumieni, a inne są zaprojektowane do działania na deskryptorach plików. Po zakończeniu wczytywania lub zapisywania pliku można zakończyć połączenie, zamykając plik. Po zamknięciu strumienia lub deskryptora pliku nie można już wykonywać na nim żadnych operacji wejścia ani wyjścia.

Metody analizy argumentów I/O

[edytuj]

Sposoby analizowania argumentów wiersza poleceń w C ( ang. parsing command line arguments or Parsing Program Arguments )[63] [64]

  • gotowe biblioteki
  • własna sposób ( ręczny). Nie jest to polecane w przypadku programów, które zostałyby przekazane komuś innemu, ponieważ jest zbyt wiele rzeczy, które mogą się nie udać lub obniżyć jakość. Popularny błąd polegający na zapominaniu o „--” ( ang. double-dash albo precyzyjniej double-hyphen) w celu zatrzymania parsowania opcji.

Argumenty I/O

[edytuj]

Argumenty I/O to argumenty programu , czyli argumenty funkcji main

Ręczne I/O

[edytuj]

Etapy

  • wczytujemy parametry
  • sprawdzamy czy są poprawne
  • przetwarzamy parametry


Funkcje wyjścia

[edytuj]

Funkcja printf

[edytuj]

W przykładzie "Witaj świecie!" użyliśmy już jednej z dostępnych funkcji wyjścia, a mianowicie funkcji printf(). Z punktu widzenia swoich możliwości jest to jedna z bardziej skomplikowanych funkcji, a jednocześnie jest jedną z najczęściej używanych. Przyjrzyjmy się ponownie kodowi programu "Witaj świecie!".

 #include <stdio.h>
 
 int main(void)
 {
   printf("Witaj swiecie!\n");
   return 0;
 }

Po skompilowaniu i uruchomieniu, program wypisze na ekranie:

Witaj swiecie!


W naszym przykładowym programie, chcąc by funkcja printf() wypisała tekst na ekranie, umieściliśmy go w cudzysłowach wewnątrz nawiasów.

Ogólnie, wywołanie funkcji printf() wygląda następująco:

printf(format, argument1, argument2, ...);

czyli funkcja może przyjąć zmienną liczbę argumentów.


Przykładowo:

 int i = 500;
 printf("Liczbami całkowitymi są na przykład %i oraz %i.\n", 1, i);

wypisze

Liczbami całkowitymi są na przykład 1 oraz 500.

Format to napis ujęty w cudzysłowy, który określa ogólny kształt, schemat tego, co ma być wyświetlone. Format jest drukowany tak, jak go napiszemy, jednak niektóre znaki specjalne zostaną w nim podmienione na co innego. Przykładowo, znak specjalny \n jest zamieniany na znak nowej linii [66]. Natomiast procent jest podmieniany na jeden z argumentów. Po procencie następuje specyfikacja, jak wyświetlić dany argument. W tym przykładzie %i (od int) oznacza, że argument ma być wyświetlony jak liczba całkowita. W związku z tym, że \ i % mają specjalne znaczenie, aby wydrukować je, należy użyć ich podwójnie:

 printf("Procent: %% Backslash: \\");

drukuje:

Procent: % Backslash: \

(bez przejścia do nowej linii). Na liście argumentów możemy mieszać ze sobą zmienne różnych typów, liczby, napisy itp. w dowolnej liczbie. Funkcja printf przyjmie ich tyle, ile tylko napiszemy. Należy uważać, by nie pomylić się w formatowaniu:

int i = 5;
printf("%i %s %i", 5, 4, "napis"); /* powinno być: "%i %i %s" */

Przy włączeniu ostrzeżeń (opcja -Wall lub -Wformat w GCC) kompilator powinien nas ostrzec, gdy format nie odpowiada podanym elementom.

Najczęstsze użycie printf():

  • printf("%i", i); gdy i jest typu int; zamiast %i można użyć %d
  • printf("%f", i); gdy i jest typu float lub double
  • printf("%c", i); gdy i jest typu char (i chcemy wydrukować znak)
  • printf("%s", i); gdy i jest napisem (typu char*)

Funkcja printf() nie jest żadną specjalną konstrukcją języka i łańcuch formatujący może być podany jako zmienna.[67] W związku z tym możliwa jest np. taka konstrukcja:

 #include <stdio.h>
 
 int main(void)
 {
   char buf[100];
   scanf("%99s", buf); /* funkcja wczytuje tekst do tablicy buf */
   printf(buf);
   return 0;
 }

Program wczytuje tekst, a następnie wypisuje go. Jednak ponieważ znak procentu jest traktowany w specjalny sposób, toteż jeżeli na wejściu pojawi się ciąg znaków zawierający ten znak mogą się stać różne dziwne rzeczy. Między innymi z tego powodu w takich sytuacjach lepiej używać funkcji puts() lub fputs() opisanych niżej lub wywołania: printf("%s", zmienna);.

Więcej o funkcji printf()

Funkcja puts

[edytuj]

Funkcja puts() przyjmuje jako swój argument ciąg znaków, który następnie bezmyślnie wypisuje na ekran kończąc go znakiem przejścia do nowej linii. W ten sposób, nasz pierwszy program moglibyśmy napisać w ten sposób:

 #include <stdio.h>
 
 int main(void)
 {
   puts("Witaj swiecie!");
   return 0;
 }

W swoim działaniu funkcja ta jest w zasadzie identyczna do wywołania: printf("%s\n", argument); jednak prawdopodobnie będzie działać szybciej. Jedynym jej mankamentem może być fakt, że zawsze na końcu podawany jest znak przejścia do nowej linii. Jeżeli jest to efekt niepożądany (nie zawsze tak jest) należy skorzystać z funkcji fputs() opisanej niżej lub wywołania printf("%s", argument);.

Więcej o funkcji puts()

Funkcja fputs

[edytuj]

Opisując funkcję fputs() wybiegamy już trochę w przyszłość (a konkretnie do opisu operacji na plikach), ale warto o niej wspomnieć już teraz, gdyż umożliwia ona wypisanie swojego argumentu bez wypisania na końcu znaku przejścia do nowej linii:

 #include <stdio.h>
 
 int main(void)
 {
   fputs("Witaj swiecie!\n", stdout);
   return 0;
 }

W chwili obecnej możesz się nie przejmować tym zagadkowym stdout wpisanym jako drugi argument funkcji. Jest to określenie strumienia wyjściowego (w naszym wypadku standardowe wyjście - standard output).

Więcej o funkcji fputs()

Funkcja putchar

[edytuj]

Funkcja putchar() służy do wypisywania pojedynczych znaków. Przykładowo jeżeli chcielibyśmy napisać program wypisujący w prostej tabelce wszystkie liczby od 0 do 99 moglibyśmy to zrobić tak:

 #include <stdio.h>
 
 int main(void) 
 {
   int i = 0;
   for (; i<100; ++i) 
   {
     /* Nie jest to pierwsza liczba w wierszu */
     if (i % 10) 
     {
       putchar(' ');
     }
     printf("%2d", i);
     /* Jest to ostatnia liczba w wierszu */
     if ((i % 10)==9) 
     {
       putchar('\n');
     }
   }
   return 0;
 }

Więcej o funkcji putchar()

Funkcje wejścia

[edytuj]

Funkcja scanf()

[edytuj]

Teraz pomyślmy o sytuacji odwrotnej. Tym razem to użytkownik musi powiedzieć coś programowi. W poniższym przykładzie program podaje kwadrat liczby, podanej przez użytkownika:

 #include <stdio.h>
 
 int main ()
 {
   int liczba = 0;
   printf ("Podaj liczbę: ");
   scanf ("%d", &liczba);
   printf ("%dx%d=%d\n", liczba, liczba, liczba*liczba); 
   return 0;
 }

Zauważyłeś, że w tej funkcji przy zmiennej pojawił się nowy operator - & (etka). Jest on ważny, gdyż bez niego funkcja scanf() nie skopiuje odczytanej wartości liczby do odpowiedniej zmiennej! Właściwie oznacza przekazanie do funkcji adresu zmiennej, by funkcja mogła zmienić jej wartość. Nie musisz teraz rozumieć jak to się odbywa. Wszystko zostanie wyjaśnione w rozdziale Wskaźniki.

Oznaczenia są podobne takie jak przy printf(), czyli scanf("%i", &liczba); wczytuje liczbę typu int, scanf("%f", &liczba); – liczbę typu float, a scanf("%s", tablica_znaków); ciąg znaków. Ale czemu w tym ostatnim przypadku nie ma etki? Otóż, gdy podajemy jako argument do funkcji wyrażenie typu tablicowego zamieniane jest ono automatycznie na adres pierwszego elementu tablicy. Będzie to dokładniej opisane w rozdziale poświęconym wskaźnikom.

Należy jednak uważać na to ostatnie użycie. Rozważmy na przykład poniższy kod:

 #include <stdio.h>
 
 int main(void)
 {
   char tablica[100];     /* 1 */
   scanf("%s", tablica);  /* 2 */
   return 0;
 }

Robi on niewiele. W linijce 1 deklarujemy tablicę 100 znaków czyli mogącą przechować napis długości 99 znaków. Nie przejmuj się jeżeli nie do końca to wszystko rozumiesz - pojęcia takie jak tablica czy ciąg znaków staną się dla Ciebie jasne w miarę czytania kolejnych rozdziałów. W linijce 2 wywołujemy funkcję scanf(), która odczytuje tekst ze standardowego wejścia. Nie zna ona jednak rozmiaru tablicy i nie wie ile znaków może ona przechować przez co będzie czytać tyle znaków, aż napotka biały znak (format %s nakazuje czytanie pojedynczego słowa), co może doprowadzić do przepełnienia bufora. Niebezpieczne skutki czegoś takiego opisane są w rozdziale poświęconym napisom. Na chwilę obecną musisz zapamiętać, żeby zaraz po znaku procentu podawać maksymalną liczbę znaków, które może przechować bufor, czyli liczbę o jeden mniejszą, niż rozmiar tablicy. Bezpieczna wersją powyższego kodu jest:

 #include <stdio.h>
 
 int main(void)
 {
   char tablica[100];
   scanf("%99s", tablica);
   return 0;
 }

Funkcja scanf() zwraca liczbę poprawnie wczytanych zmiennych lub EOF jeżeli nie ma już danych w strumieniu lub nastąpił błąd. Załóżmy dla przykładu, że chcemy stworzyć program, który odczytuje po kolei liczby i wypisuje ich trzecie potęgi. W pewnym momencie dane się kończą lub jest wprowadzana niepoprawna dana i wówczas nasz program powinien zakończyć działanie. Aby to zrobić, należy sprawdzać wartość zwracaną przez funkcję scanf() w warunku pętli:

 #include <stdio.h>
 
 int main(void)
 {
   int n;
   while (scanf("%d", &n)==1) 
   {
     printf("%d\n", n*n*n);
   }
   return 0;
 }

Podobnie możemy napisać program, który wczytuje po dwie liczby i je sumuje:

 #include <stdio.h>
 
 int main(void)
 {
   int a, b;
   while (scanf("%d %d", &a, &b)==2) 
   {
     printf("%d\n", a+b);
   }
   return 0;
 }

Rozpatrzmy teraz trochę bardziej skomplikowany przykład. Otóż, ponownie jak poprzednio nasz program będzie wypisywał trzecią potęgę podanej liczby, ale tym razem musi ignorować błędne dane (tzn. pomijać ciągi znaków, które nie są liczbami) i kończyć działanie tylko w momencie, gdy nastąpi błąd odczytu lub koniec pliku[68].

 #include <stdio.h>
 
 int main(void)
 {
   int result, n;
   do 
   {
     result = scanf("%d", &n);
     if (result) /* result to to samo co result!=0 */
       printf("%d\n", n*n*n);
     else
       result = scanf("%*s");
   } 
   while (result!=EOF);
   return 0;
 }

Zastanówmy się przez chwilę co się dzieje w programie. Najpierw wywoływana jest funkcja scanf() i następuje próba odczytu liczby typu int. Jeżeli funkcja zwróciła 1 to liczba została poprawnie odczytana i następuje wypisanie jej trzeciej potęgi. Jeżeli funkcja zwróciła 0 to na wejściu były jakieś dane, które nie wyglądały jak liczba. W tej sytuacji wywołujemy funkcję scanf() z formatem odczytującym dowolny ciąg znaków nie będący białymi znakami z jednoczesnym określeniem, żeby nie zapisywała nigdzie wyniku. W ten sposób niepoprawnie wpisana dana jest omijana. Pętla główna wykonuje się tak długo jak długo funkcja scanf() nie zwróci wartości EOF.

Więcej o funkcji scanf()

Funkcja gets

[edytuj]

Funkcja gets służy do wczytania pojedynczej linii. Może Ci się to wydać dziwne, ale: funkcji tej nie należy używać pod żadnym pozorem. Przyjmuje ona jeden argument - adres pierwszego elementu tablicy, do którego należy zapisać odczytaną linię - i nic poza tym. Z tego powodu nie ma żadnej możliwości przekazania do tej funkcji rozmiaru bufora podanego jako argument. Podobnie jak w przypadku scanf() może to doprowadzić do przepełnienia bufora, co może mieć tragiczne skutki. Zamiast tej funkcji należy używać funkcji fgets().

Więcej o funkcji gets()

Funkcja fgets

[edytuj]

Funkcja fgets() jest bezpieczną wersją funkcji gets(), która dodatkowo może operować na dowolnych strumieniach wejściowych. Jej użycie jest następujące:

fgets(tablica_znaków, rozmiar_tablicy_znaków, stdin);

Na chwilę obecną nie musisz się przejmować ostatnim argumentem (jest to określenie strumienia, w naszym przypadku standardowe wejście - standard input). Funkcja czyta tekst aż do napotkania znaku przejścia do nowej linii, który także zapisuje w wynikowej tablicy (funkcja gets() tego nie robi). Jeżeli brakuje miejsca w tablicy to funkcja przerywa czytanie, w ten sposób, aby sprawdzić czy została wczytana cała linia czy tylko jej część należy sprawdzić czy ostatnim znakiem nie jest znak przejścia do nowej linii. Jeżeli nastąpił jakiś błąd lub na wejściu nie ma już danych funkcja zwraca wartość NULL.

 #include <stdio.h>
 
 int main(void) {
   char buffer[128], whole_line = 1, *ch;
   while (fgets(buffer, sizeof buffer, stdin)) { /* 1 */
     if (whole_line) {                           /* 2 */
       putchar('>');
       if (buffer[0]!='>') {
         putchar(' ');
       }
     }
     fputs(buffer, stdout);                      /* 3 */
     for (ch = buffer; *ch && *ch!='\n'; ++ch);  /* 4 */
     whole_line = *ch == '\n';
   }
   if (!whole_line) {
     putchar('\n');
   }
   return 0;
 }

Powyższy kod wczytuje dane ze standardowego wejścia - linia po linii - i dodaje na początku każdej linii znak większości, po którym dodaje spację jeżeli pierwszym znakiem na linii nie jest znak większości. W linijce 1 następuje odczytywanie linii. Jeżeli nie ma już więcej danych lub nastąpił błąd wejścia funkcja zwraca wartość NULL, która ma logiczną wartość 0 i wówczas pętla kończy działanie. W przeciwnym wypadku funkcja zwraca po prostu pierwszy argument, który ma wartość logiczną 1. W linijce 2 sprawdzamy, czy poprzednie wywołanie funkcji wczytało całą linię, czy tylko jej część - jeżeli całą to teraz jesteśmy na początku linii i należy dodać znak większości. W linii 3 najzwyczajniej w świecie wypisujemy linię. W linii 4 przeszukujemy tablicę znak po znaku, aż do momentu, gdy znajdziemy znak o kodzie 0 kończącym ciąg znaków albo znak przejścia do nowej linii. Ten drugi przypadek oznacza, że funkcja fgets() wczytała całą linię.

Więcej o funkcji fgets()

Funkcja getchar()

[edytuj]

Jest to bardzo prosta funkcja, wczytująca 1 znak z klawiatury. W wielu przypadkach dane mogą być buforowane przez co wysyłane są do programu dopiero, gdy bufor zostaje przepełniony lub na wejściu jest znak przejścia do nowej linii. Z tego powodu po wpisaniu danego znaku należy nacisnąć klawisz enter, aczkolwiek trzeba pamiętać, że w następnym wywołaniu zostanie zwrócony znak przejścia do nowej linii. Gdy nastąpił błąd lub nie ma już więcej danych funkcja zwraca wartość EOF (która ma jednak wartość logiczną 1 toteż zwykła pętla while (getchar()) nie da oczekiwanego rezultatu):

 #include <stdio.h>
 
 int main(void)
 {
   int c;
   while ((c = getchar())!=EOF) {
     if (c==' ') {
       c = '_';
     }
     putchar(c);
   }
   return 0;
 }

Ten prosty program wczytuje dane znak po znaku i zamienia wszystkie spacje na znaki podkreślenia. Może wydać się dziwne, że zmienną c zdefiniowaliśmy jako trzymającą typ int, a nie char. Właśnie taki typ (tj. int) zwraca funkcja getchar() i jest to konieczne ponieważ wartość EOF wykracza poza zakres wartości typu char (gdyby tak nie było to nie byłoby możliwości rozróżnienia wartości EOF od poprawnie wczytanego znaku). Więcej o funkcji getchar()


Przypisy

  1. pliki te posiadają najczęściej rozszerzenie .h (lub .hpp, które zwykło się stosować w języku C++). Rozszerzenie nie ma swych "technicznych" korzeni - jest to tylko pewna konwencja.
  2. Przed procesem kompilacji, w miejsce tej dyrektywy wstawiana jest treść podanego pliku nagłówkowego, dostarczając deklaracji funkcji
  3. stackoverflow question: what-is-the-difference-between-a-definition-and-a-declaration
  4. the C data model by Jens Gustedt
  5. The ``Clockwise/Spiral Rule By David Anderson
  6. Reading C type declarations by Steve Friedl
  7. c4learn c-variable-nameing-rules
  8. Recommended C Style and Coding Standards
  9. Recommended C Style and Coding Standards
  10. about c by weebly
  11. Naming guidelines for professional programmers Copyright ©2017 by Peter Hilton and Felienne Hermans
  12. how-to-better-name-your-functions-and-variables by Friskovec Miha
  13. Nazwy zmiennych, notacje i konwencje nazewnicze - Mateusz Skalski
  14. Kurs języka C. Autor artykułu: mgr Jerzy Wałaszek
  15. Hungarian Notation by Charles Simonyi Microsoft Corporation Reprinted November 1999
  16. Literał w wikipedii
  17. cppreference: c language: compound literal
  18. cpp reference : c language - type
  19. cppreference: c types integer
  20. cppreference: integer c types
  21. Fixed-width_integer_types in english wiki
  22. The Floating-Point Guide
  23. | Displaying the Raw Fields of a Floating-Point Number By Rick Regan (Published May 20th, 2009)
  24. wartości specjalne liczbn zmiennoprzecinkowych w wiki[edii
  25. cppreference : ascii
  26. stackoverflow question: how-to-find-the-length-of-a-character
  27. opis size_t w cpp0x
  28. About size_t and ptrdiff_t, Andrey Karpov
  29. Why size_t matters, Dan Saks
  30. stackoverflow question : what-is-size-t-in-c
  31. Wiąże się to z pewnymi uwarunkowaniami historycznymi. Podręcznik do języka C duetu K&R zakładał, że typ int miał się odnosić do typowej dla danego procesora długości liczby całkowitej. Natomiast jeśli procesor mógł obsługiwać typy dłuższe lub krótsze stosownego znaczenia nabierały modyfikatory short i long. Dobrym przykładem może być architektura i386, która umożliwia obliczenia na liczbach 16-bitowych. Dlatego też modyfikator short powoduje skrócenie zmiennej do 16 bitów.
  32. wikipedia : Konwersja typu
  33. C++ and simple type conversion - University of CambridgeDepartment of Engineering
  34. frama-c : Overflow-float-integer
  35. libc manual : Integer-Division
  36. stackoverflow question: what-is-the-behavior-of-integer-division
  37. delftstack : c-integer-division by Waqar Aslam Sep 16, 2022
  38. Bitwise_operations_in_C ( english wikipedia)
  39. C MANIPULACJE BITAMI - Karol Kuczmarski „Xion”
  40. Bit Twiddling Hacks By Sean Eron Anderson
  41. Bitwise Operators by joe_query
  42. Język C - Herbert Schildt, Oficyna Wydawnicza LTP, Warszawa 2002
  43. quora : What-are-useful-tricks-in-C++-or-C-that-beginners-rarely-know ?
  44. XOR Encryption by Alex Allain
  45. bit-array in c by Shun Y. Cheung
  46. geeksforgeeks: bits-manipulation-important-tactics
  47. geeksforgeeks : bit-tricks-competitive-programming
  48. Bit Operations in C/C++ by Robert B. Heckendorn University of Idaho
  49. ocfreaks tutorial: embedded-programming-basics-in-c-bitwise-operations
  50. geeksforgeeks : left-shift-right-shift-operators-c-cpp
  51. Comparing floating point numbers, Bruce Dawson
  52. Stackoverflow : Using true and false in C
  53. Logiczny typ danych w Wikipedii
  54. Stackoverflow : Using boolean values in C
  55. Stackoverflow : Is bool a native C type?
  56. Short-circuit evaluation in english wikipedia
  57. Wikipedia : osobliwości C
  58. Jest to zaszłość historyczna z czasów, gdy nie było logicznych operatorów && oraz || i zamiast nich stosowano operatory bitowe & oraz |.
  59. C reading complex pointer expression
  60. The ``Clockwise/Spiral Rule By David Anderson
  61. Tak naprawdę podobną operacje, możemy wykonać za pomocą polecenia goto. W praktyce jednak stosuje się zasadę, że break stosuje się do przerwania działania pętli i wyjścia z niej, goto stosuje się natomiast wtedy, kiedy chce się wydostać z kilku zagnieżdżonych pętli za jednym zamachem. Do przerwania pracy pętli mogą nam jeszcze posłużyć polecenia exit() lub return, ale wówczas zakończymy nie tylko działanie pętli, ale i całego programu/funkcji.
  62. Żartobliwie można powiedzieć, że stosując pętlę nieskończoną to najlepiej korzystać z pętli for(;;){}, gdyż wymaga ona napisania najmniejszej liczby znaków w porównaniu do innych konstrukcji.
  63. gnu libc manual : Parsing-Program-Arguments
  64. stackoverflow question: parsing-command-line-arguments-in-c
  65. | POSIX.1-2017 from The Open Group Base Specifications Issue 7, 2018 edition. 12.2 Utility Syntax Guidelines. Guideline 10
  66. Zmiana ta następuje w momencie kompilacji programu i dotyczy wszystkich literałów napisowych. Nie jest to jakaś szczególna własność funkcji printf(). Więcej o tego typu sekwencjach i ciągach znaków w szczególności opisane jest w rozdziale Napisy.
  67. unix.com : passing-printf-formatting-parameters-variables
  68. Jak rozróżniać te dwa zdarzenia dowiesz się w rozdziale Czytanie i pisanie do plików.


Funkcje

[edytuj]

Wstęp

[edytuj]

W matematyce pod pojęciem funkcji rozumiemy twór, który pobiera pewną liczbę argumentów i zwraca wynik[1]. Jeśli dla przykładu weźmiemy funkcję sin(x) to x będzie zmienną rzeczywistą, która określa kąt, a w rezultacie otrzymamy inną liczbę rzeczywistą - sinus tego kąta.

W C funkcja (czasami nazywana podprogramem, rzadziej procedurą) to wydzielona część programu, która przetwarza argumenty i ewentualnie zwraca wartość, która następnie może być wykorzystana jako argument w innych działaniach lub funkcjach. Funkcja może posiadać własne zmienne lokalne. W odróżnieniu od funkcji matematycznych, funkcje w C mogą zwracać dla tych samych argumentów różne wartości.

Po lekturze poprzednich części podręcznika zapewne mógłbyś podać kilka przykładów funkcji, z których korzystałeś. Były to np.

  • funkcja printf(), drukująca tekst na ekranie, czy
  • funkcja main(), czyli główna funkcja programu.

Główną motywacją tworzenia funkcji jest unikanie powtarzania kilka razy tego samego kodu. W poniższym fragmencie:

for(i=1; i <= 5; ++i) {
  printf("%d ", i*i);
}
for(i=1; i <= 5; ++i) {
  printf("%d ", i*i*i);
} 
for(i=1; i <= 5; ++i) {
  printf("%d ", i*i);
}

widzimy, że pierwsza i trzecia pętla for są takie same. Zamiast kopiować fragment kodu kilka razy (co jest mało wygodne i może powodować błędy) lepszym rozwiązaniem mogłoby być wydzielenie tego fragmentu tak, by można go było wywoływać kilka razy. Tak właśnie działają funkcje.

Innym, nie mniej ważnym powodem używania funkcji jest rozbicie programu na fragmenty wg ich funkcjonalności. Oznacza to, że jeden duży program dzieli się na mniejsze funkcje, które są "wyspecjalizowane" w wykonywaniu określonych czynności. Dzięki temu łatwiej jest zlokalizować błąd. Ponadto takie funkcje można potem przenieść do innych programów.


Formalnie wyróżniamy:

  • deklaracja funkcji
  • definicja funkcji
  • wywołanie funkcji

Tworzenie funkcji

[edytuj]

Dobrze jest uczyć się na przykładach. Rozważmy następujący kod:

int iloczyn (int x, int y)
{
  int iloczyn_xy;
  iloczyn_xy = x*y;
  return iloczyn_xy;
}

int iloczyn (int x, int y) to nagłówek funkcji, który opisuje, jakie argumenty przyjmuje funkcja i jaką wartość zwraca (funkcja może przyjmować wiele argumentów, lecz może zwracać tylko jedną wartość)[2]. Na początku podajemy typ zwracanej wartości - u nas int. Następnie mamy nazwę funkcji i w nawiasach listę argumentów.

Ciało funkcji (czyli wszystkie wykonywane w niej operacje) umieszczamy w nawiasach klamrowych. Pierwszą instrukcją jest deklaracja zmiennej - jest to zmienna lokalna, czyli niewidoczna poza funkcją. Dalej przeprowadzamy odpowiednie działania i zwracamy rezultat za pomocą instrukcji return.

nazwa

[edytuj]

Cel i zasady nadawania nazw:[3]

  • Wybierz słowo mające znaczenie (podaj kontekst): jednoznacznie i precyzyjnie opisywać koncept który nazywają[4]
  • Unikaj nazw ogólnych (takich jak tmp)
  • Nazwa funkcji nie może być taka sama jak słowo kluczowe języka C oraz jak nazwa innej funkcji, która została wcześniej zdefiniowana w programie[5]
  • Dołącz dodatkowe informacje do nazwy (użyj sufiksu lub prefiksu)
  • Nie rób zbyt długich ani zbyt krótkich imion
  • Używaj spójnego formatowania


Przykłady W nazwie są trzy człony określające:[6]

module_object_create()

Ten sposób podkreśla miejsce funkcji w strukturze modułu ( tak jak katalog)

Definicja

[edytuj]

Funkcję w języku C tworzy się następująco:

 typ identyfikator (typ1 argument1, typ2 argument2, typ_n argument_n)
 {
   /* instrukcje */
 }

Oczywiście istnieje możliwość utworzenia funkcji, która nie posiada żadnych argumentów. Definiuje się ją tak samo, jak funkcję z argumentami z tą tylko różnicą, że między okrągłymi nawiasami nie znajduje się żaden argument lub pojedyncze słówko void - w definicji funkcji nie ma to znaczenia, jednak w deklaracji puste nawiasy oznaczają, że prototyp nie informuje jakie argumenty przyjmuje funkcja, dlatego bezpieczniej jest stosować słówko void.

Funkcje definiuje się poza główną funkcją programu (main). W języku C nie można tworzyć zagnieżdżonych funkcji (funkcji wewnątrz innych funkcji).


Struktura definicji

return_type function_name( parameter_list ) {
   // body_of_the_function
}

Stary sposób definiowania funkcji

[edytuj]

Zanim powstał standard ANSI C, w liście parametrów nie podawało się typów argumentów, a jedynie ich nazwy. Również z tamtych czasów wywodzi się oznaczenie, iż puste nawiasy (w prototypie funkcji, nie w definicji) oznaczają, że funkcja przyjmuje nieokreśloną liczbę argumentów. Tego archaicznego sposobu definiowania funkcji nie należy już stosować, ale ponieważ w swojej przygodzie z językiem C Czytelnik może się na nią natknąć, a co więcej standard nadal (z powodu zgodności z wcześniejszymi wersjami) dopuszcza taką deklarację to należy tutaj o niej wspomnieć. Otóż wygląda ona następująco:

typ_zwracany nazwa_funkcji(argument1, argument2, argumentn)
  typ1 argumenty /*, ... */;
  typ2 argumenty /*, ... */;
  /* ... */
{
  /* instrukcje */
}

Na przykład wcześniejsza funkcja iloczyn wyglądałaby następująco:

int iloczyn(x, y)
  int x, y;
{
  int iloczyn_xy;
  iloczyn_xy = x*y;
  return iloczyn_xy;
}

Najpoważniejszą wadą takiego sposobu jest fakt, że w prototypie funkcji nie ma podanych typów argumentów, przez co kompilator nie jest w stanie sprawdzić poprawności wywołania funkcji. Naprawiono to (wprowadzając definicje takie jak je znamy obecnie) najpierw w języku C++, a potem rozwiązanie zapożyczono w standardzie ANSI C z 1989 roku.

Deklarowanie funkcji

[edytuj]

Czasami możemy chcieć przed napisaniem funkcji poinformować kompilator, że dana funkcja istnieje. Niekiedy kompilator może zaprotestować, jeśli użyjemy funkcji przed określeniem, jaka to funkcja, na przykład:

int a()
{
  return b(0);
}

int b(int p)
{
  if( p == 0 )
    return 1;
  else
    return a();
}
 
int main()
{
  return b(1);
}

W tym przypadku nie jesteśmy w stanie zamienić a i b miejscami, bo obie funkcje korzystają z siebie nawzajem. Rozwiązaniem jest wcześniejsze zadeklarowanie funkcji. Deklaracja funkcji (zwana czasem prototypem) to po prostu przekopiowana pierwsza linijka funkcji (przed otwierającym nawiasem klamrowym) z dodatkowo dodanym średnikiem na końcu. W naszym przykładzie wystarczy na samym początku wstawić:

int b(int p);

W deklaracji można pominąć nazwy parametrów funkcji:

int b(int);

Bardzo częstym zwyczajem jest wypisanie przed funkcją main samych prototypów funkcji, by ich definicje umieścić po definicji funkcji main, np.:

int a(void);
int b(int p);

int main()
{
  return b(1);
}

int a()
{
  return b(0);
}

int b(int p)
{
  if( p == 0 )
    return 1;
  else
    return a();
}

Z poprzednich rozdziałów pamiętasz, że na początku programu dołączaliśmy tzw. pliki nagłówkowe. Zawierają one właśnie prototypy funkcji i ułatwiają pisanie dużych programów. Dalsze informacje o plikach nagłówkowych zawarte są w rozdziale Tworzenie bibliotek.

argumenty funkcji

[edytuj]
  • liczba
    • stała liczba argumentów
      • bez argumentów ( zero argumentów) void
      • 1 i więcej argumentów
    • zmienna liczba argumentów (ang functions which take a variable number of arguments = variadic function)
  • sposób wczytywania[7]
    • za pomocą wartości (ang. call by value)
    • za pomocą wskaźnika (ang. call by reference)

Zobacz również

Zmienna liczba argumentów

[edytuj]

Zauważyłeś zapewne, że używając funkcji printf() lub scanf() po argumencie zawierającym tekst z odpowiednimi modyfikatorami mogłeś podać praktycznie nieograniczoną liczbę argumentów. Zapewne deklaracja obu funkcji zadziwi Cię jeszcze bardziej:

int printf(const char *format, ...);
int scanf(const char *format, ...);

Jak widzisz w deklaracji zostały użyte trzy kropki. Otóż język C ma możliwość przekazywania teoretycznie nieograniczonej liczby argumentów do funkcji (jedynym ograniczeniem jest rozmiar stosu programu). Cała zabawa polega na tym, aby umieć dostać się do odpowiedniego argumentu oraz poznać jego typ (używając funkcji printf, mogliśmy wpisać jako argument dowolny typ danych). Do tego celu możemy użyć wszystkich ciekawostek, zawartych w pliku nagłówkowym stdarg.h.

Załóżmy, że chcemy napisać prostą funkcję, która dajmy na to, mnoży wszystkie swoje argumenty (zakładamy, że argumenty są typu int). Przyjmujemy przy tym, że ostatni argument będzie 0. Będzie ona wyglądała tak:

 #include <stdarg.h>
 
 int mnoz (int pierwszy, ...)
 {
   va_list arg;
   int iloczyn = 1, t;
   va_start (arg, pierwszy);
   for (t = pierwszy; t; t = va_arg(arg, int)) {
     iloczyn *= t;
   } 
   va_end (arg);
   return iloczyn;
 }

va_list oznacza specjalny typ danych, w którym przechowywane będą argumenty, przekazane do funkcji. "va_start" inicjuje arg do dalszego użytku. Jako drugi parametr musimy podać nazwę ostatniego znanego argumentu funkcji. Makropolecenie va_arg odczytuje kolejne argumenty i przekształca je do odpowiedniego typu danych. Na zakończenie używane jest makro va_end - jest ono obowiązkowe!

Oczywiście, tak samo jak w przypadku funkcji printf() czy scanf(), argumenty nie muszą być takich samych typów. Rozważmy dla przykładu funkcję, podobną do printf(), ale znacznie uproszczoną:

#include <stdarg.h>

void wypisz(const char *format, ...) {
  va_list arg;
  va_start (arg, format);
  for (; *format; ++format) {
    switch (*format) {
    case 'i': printf("%d" , va_arg(arg, int)); break;
    case 'I': printf("%u" , va_arg(arg, unsigned)); break;
    case 'l': printf("%ld", va_arg(arg, int)); break;
    case 'L': printf("%lu", va_arg(arg, unsigned long)); break;
    case 'f': printf("%f" , va_arg(arg, double)); break;
    case 'x': printf("%x" , va_arg(arg, unsigned)); break;
    case 'X': printf("%X" , va_arg(arg, unsigned)); break;
    case 's': printf("%s" , va_arg(arg, const char *)); break;
    default : putc(*format);
    }
  }
  va_end (arg);
}

Przyjmuje ona jako argument ciąg znaków, w których niektóre instruują funkcję, by pobrała argument i go wypisała. Nie przejmuj się jeżeli nie rozumiesz wyrażeń *format i ++format. Istotne jest to, że pętla sprawdza po kolei wszystkie znaki formatu.


Zobacz również:

  • Funkcje o zmiennej liczbie argumentów (wariadyczne) ( ang. Variadic functions or varargs functions )[8] [9]
  • makra ( ang. function macro) zdefiniowane w <stdarg.h>[10]
    • va_start - umożliwia dostęp do zmiennych argumentów funkcji
    • va_arg - dostęp do następnego argumentu funkcji wariadycznej
    • va_copy - makes a copy of the variadic function arguments (C99)
    • va_end - ends traversal of the variadic function arguments
  • typ : va_list - holds the information needed by va_start, va_arg, va_end, and va_copy
  • Wiele starszych dialektów języka C zapewnia podobny, ale niekompatybilny mechanizm definiowania funkcji ze zmienną liczbą argumentów przy użyciu varargs.h ( legacy). Nie zaleca się używania <varargs.h>


Pomoc:

  • offline:
    • W systemach uniksowych możesz uzyskać pomoc dzięki narzędziu man, przykładowo pisząc: man va_start

Wywoływanie funkcji

[edytuj]

Funkcje wywołuje się następująco:

identyfikator (argument1, argument2, argumentn);

Jeśli chcemy, aby przypisać zmiennej wartość, którą zwraca funkcja, należy napisać tak:

zmienna = funkcja (argument1, argument2, argumentn);

Przykładowo, mamy funkcję:

void pisz_komunikat()
{
  printf("To jest komunikat\n");
}

Jeśli teraz ją wywołamy:

pisz_komunikat;   /* ŹLE    */
pisz_komunikat(); /* dobrze */

to pierwsze polecenie nie spowoduje wywołania funkcji. Dlaczego? Aby kompilator C zrozumiał, że chodzi nam o wywołanie funkcji, musimy po jej nazwie dodać nawiasy okrągłe, nawet, gdy funkcja nie ma argumentów. Użycie samej nazwy funkcji ma zupełnie inne znaczenie - oznacza pobranie jej adresu. W jakim celu? O tym będzie mowa w rozdziale Wskaźniki.

Przykład

A oto działający przykład, który demonstruje wiadomości podane powyżej:

 #include <stdio.h>
 
 int suma (const int a, const int b)
 {
   return a+b;
 }
 
 int main ()
 {
   int m = suma (4, 5);
   printf ("4+5=%d\n", m);
   return 0;
 }

Zwracanie wartości

[edytuj]
  • za pomocą return ( tylko jedna wartość )
  • nie zwracanie żadnej wartość ( zwracany typ = void), bez return,
  • zwracanie kilku wartości
    • struktury
    • wskaźniki

słowo kluczowe return

[edytuj]

return to słowo kluczowe języka C.

W przypadku funkcji służy ono do:

  • przerwania funkcji (i przejścia do następnej instrukcji w funkcji wywołującej)
  • zwrócenia wartości.

W przypadku procedur powoduje przerwania procedury bez zwracania wartości.

Użycie tej instrukcji jest bardzo proste i wygląda tak:

return zwracana_wartość;

lub dla procedur:

return;

Możliwe jest użycie kilku instrukcji return w obrębie jednej funkcji. Wielu programistów uważa jednak, że lepsze jest użycie jednej instrukcji return na końcu funkcji, gdyż ułatwia to śledzenie przebiegu programu.

Standardowo zwracana wartość

[edytuj]

W C zwykle przyjmuje się, że 0 oznacza poprawne zakończenie funkcji:

return 0; /* funkcja zakończona sukcesem */

a inne wartości oznaczają niepoprawne zakończenie:

return 1; /*funkcja zakończona niepowodzeniem */

Ta wartość może być wykorzystana przez inne instrukcje, np. if .

Procedura czyli jak nie zwracać wartości

[edytuj]

Przyjęło się, że procedura od funkcji różni się tym, że ta pierwsza nie zwraca żadnej wartości. Zatem, aby stworzyć procedurę należy napisać:

void identyfikator (typ1 argument1, typ2 argument2, typn argument_n)
{
  /* instrukcje */
}

void (z ang. pusty, próżny) jest słowem kluczowym mającym kilka znaczeń, w tym przypadku oznacza "brak wartości".

Generalnie, w terminologii C pojęcie "procedura" nie jest używane, mówi się raczej "funkcja zwracająca void".


Czasem procedura nie zwraca wartości ale zmienia wartość argumentów.

Jak zwrócić kilka wartości?

[edytuj]

Jeśli chcesz zwrócić z funkcji kilka wartości, musisz zrobić to w trochę inny sposób. Generalnie możliwe są dwa podejścia:

  • "upakowanie" zwracanych wartości – można stworzyć tak zwaną strukturę, która będzie przechowywała kilka zmiennych (jest to opisane w rozdziale Typy złożone).
  • zwracanie jednej z wartości w normalny sposób ( return), a pozostałych jako parametrów. Jeśli chcesz zobaczyć przykład, możesz przyjrzeć się funkcji scanf() z biblioteki standardowej.

Za pomocą struktur

[edytuj]

Przykład [11]

#include<stdio.h>
 
typedef struct{
	int integer;
	float decimal;
	char letter;
	char string[100];
	double bigDecimal;
}Composite;
 
Composite f()
{
	Composite C = {1, 2.3, 'a', "Hello World", 45.678};
	return C;
}
 
 
int main()
{
	Composite C;
          

        C = f();
 
	printf("Values from a function returning a structure : { %d, %f, %c, %s, %f}\n", C.integer, C.decimal, C.letter, C.string, C.bigDecimal);
 
	return 0;
}

Przykład z SO[12]

Za pomocą wskaźników (parametrów)

[edytuj]

Gdy wywołujemy funkcję, wartość argumentów, z którymi ją wywołujemy, jest kopiowana do funkcji. Kopiowana - to znaczy, że nie możemy normalnie zmienić wartości zewnętrznych dla funkcji zmiennych. Formalnie mówi się, że w C argumentyprzekazywane przez wartość, czyli wewnątrz funkcji operujemy tylko na ich kopiach.

Możliwe jest modyfikowanie zmiennych przekazywanych do funkcji jako parametry - ale do tego w C potrzebne są wskaźniki.

Przykłady
[edytuj]

Funkcja swap wczytuje 2 wartości i zamienia je miejscami, korzystając ze wskaźników

#include <stdio.h>
// gcc s.c -Wall
// ./a.out
void swap (int *a, int *b)  {
    int temp = *a;
    *a = *b;
    *b = temp;
}


int main()
{
int x=3, y=4;

printf("x=%d ; y= %d\n", x,y); 
swap(&x, &y);
printf("x=%d ; y= %d\n", x,y); 


 return 0;

}

Wynik programu :


 x=3 ; y= 4
 x=4 ; y= 3

Jak widzimy funkcja zmienia wartości swoich argumentów. Porównaj: stałe argumenty.


Tutaj funkcja ma 2 argumenty

  • pierwszy jest stały = funkcja nie zmienia jego wartosci
  • drugi jest wskaźnikiem. Funkcja zmienia jego wartości
#include <stdio.h>
/ gcc s.c -Wall 
// ./a.out
void f (const double a, double *b)  {
    
  *b = - (*b)  * 2.0;  // zmieniamy wartość b korzystająć tylko z b
  *b += a;  // zmieniamy wartość b korzystająć ze zmiennych  a i b; zmienna a się nie zmienia 
    
}


int main()
{
  const double a = 0.2;
  double b= 1.0;

  printf("a = %f \t b =%+f\n", a, b); 
  f(a, &b);
  printf("a = %f \t b =%+f\n", a, b); 

  return 0;
}


Wynik:

 gcc s.c -Wall -lm
 ./a.out
 
 a = 0.200000 	 b =+1.000000
 a = 0.200000 	 b =-1.800000
Tablice jako parametr funkcji
[edytuj]

Istnieją 3 sposoby :

  • wskaźnik
  • tablica z podaną wielkością
  • tablica bez podanej wielkości


// https://www.tutorialspoint.com/cprogramming/c_passing_arrays_to_functions.htm 

void myFunction(int *param) // Formal parameters as a pointer  = 

void myFunction(int param[10]) // Formal parameters as a sized array −

void myFunction(int param[]) //Formal parameters as an unsized array −

Ponieważ nazwa tablicy jest wskaźnikiem do jej pierwszego elementu, to możemy korzystać z tablic w ten sposób:[13]

#include<stdio.h>

void read(int c[],int i)
{
    int j;
    for(j=0;j<i;j++)
        scanf("%d",&c[j]);
    fflush(stdin);
}

void display(int d[],int i)
{
    int j;
    for(j=0;j<i;j++)
        printf("%d ",d[j]);
    printf("\n");
}
   
  
int main()
{
    int a[5];
    printf("Wprowadź 5 elementów listy \n");
    read(a,5);
    printf("Elementami listy są : \n");
    display(a,5);
    return 0;
}

Funkcje nie tylko mają dostęp do tablicy, ale i mogą ją zmieniać. Zobacz również :rozmiar tablicy przekazywanej do funkcji


Przekazywanie wielowymiarowych tablic:

Specjalne funkcje

[edytuj]

Funkcja main()

[edytuj]

Do tej pory we wszystkich programach istniała funkcja main(). Po co tak właściwie ona jest? Otóż jest to funkcja, która zostaje wywołana przez fragment kodu inicjującego pracę programu. Kod ten tworzony jest przez kompilator i nie mamy na niego wpływu.

Istotne jest, że każdy program w języku C musi zawierać funkcję main().

Jak napisać dobrą funkcję main ?[17]

Celem funkcji main() jest:[18]

  • zebranie argumentów programu podanych przez użytkownika
  • wykonanie minimalnej walidacji danych wejściowych
  • następnie przekazanie zebranych argumentów do funkcji, które będą ich używać


Prototypy

[edytuj]

Istnieją dwa możliwe prototypy (nagłówki) omawianej funkcji:

  • int main(void);
  • int main(int argc, char **argv); [19]

Argumenty

[edytuj]

Argumenty funkcji main:

  • Pierwszy argument argc (ang. argument count )[20] określa liczbę argumentów programu. Jest to liczba nieujemną. Argument programu to ciągów znaków przechowywanych jest w tablicy argv.
  • drugi argument argv (ang. argument vector ) to wskaźnik tablicy zawierającej argumenty programu

Właściwości tablicy argv

  • Pierwszym elementem tablicy czyli argv[0] (o ile istnieje[21]) jest nazwa programu[22] czy komenda, którą program został uruchomiony.
  • Pozostałe przechowują argumenty podane przy uruchamianiu programu
  • ostatnią wartością tablicy czyli argv[argc] ma zawsze wartość NULL

Zazwyczaj jeśli program uruchomimy poleceniem:

program argument1 argument2 

to argc będzie równe 3 (2 argumenty + nazwa programu), a argv będzie zawierać napisy program, argument1, argument2 umieszczone w tablicy indeksowanej od 0 do 2.

Weźmy dla przykładu program, który wypisuje to, co otrzymuje w argumentach argc i argv:

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

int main(int argc, char **argv) {
  int i;
  for (i = 0; i<argc; ++i) {
    printf("%s\n", argv[i]);
  }
  return EXIT_SUCCESS;
}

Uruchomiony w systemie typu UNIX poleceniem ./test foo bar baz powinien wypisać:

./test
foo
bar
baz

Na razie nie musisz rozumieć powyższych kodów i opisów, gdyż odwołują się do pojęć takich jak tablica oraz wskaźnik, które opisane zostaną w dalszej części podręcznika.

Jeśli program nie wczytuje  żadnych argumentów to : [23]

int main(int argc, char **argv)
{
  (void) argc;
  (void) argv;
  return 0;
}

rekurencja

[edytuj]

Co ciekawe, funkcja main nie różni się zanadto od innych funkcji i tak jak inne może wołać sama siebie (patrz rekurencja niżej), przykładowo powyższy program można zapisać tak[24]:

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

int main(int argc, char **argv) {
  if (*argv) {
    puts(*argv);
    return main(argc-1, argv+1);
  } else {
    return EXIT_SUCCESS;
  }
}


Wartość

[edytuj]

Ostatnią rzeczą dotyczącą funkcji main jest zwracana przez nią wartość.[25] Już przy omawianiu pierwszego programu wspomniane zostało, że jedynymi wartościami, które znaczą zawsze to samo we wszystkich implementacjach języka są 0, EXIT_SUCCESS i EXIT_FAILURE[26] zdefiniowane w pliku nagłówkowym stdlib.h. Wartość 0 i EXIT_SUCCESS oznaczają poprawne zakończenie programu (co wcale nie oznacza, że makro EXIT_SUCCESS ma wartość zero), natomiast EXIT_FAILURE zakończenie błędne. Wszystkie inne wartości są zależne od implementacji.


Co się dzieje przed funcją main?

[edytuj]

usage

[edytuj]

Zawsze dołączamy funkcję usage(), którą wywołuje główna funkcja programu main(), gdy nie rozumie czegoś, co przekazałeś z wiersza poleceń[27]

void usage(char *progname, int opt) {
   fprintf(stderr, "failure\n");
   exit(EXIT_FAILURE);
   /* NOTREACHED */
}
if(argc!=2){printf("Usage:...");exit(1);}

Funkcje rekurencyjne

[edytuj]

Poniżej przekażemy ci parę bardziej zaawansowanych informacji o funkcjach w C, jeśli nie masz ochoty wgłębiać się w szczegóły, możesz spokojnie pominąć tę część i wrócić tu później.

Język C ma możliwość tworzenia tzw. funkcji rekurencyjnych. Jest to funkcja, która w swojej własnej definicji (ciele) wywołuje samą siebie. Najbardziej klasycznym przykładem może tu być silnia. Napiszmy sobie zatem naszą funkcję rekurencyjną, która oblicza silnię:

int silnia (int liczba)
{
  int sil;
  if (liczba<0) return 0; /* wywołanie jest bezsensowne, zwracamy 0 jako kod błędu */
  if (liczba==0 || liczba==1) return 1;
  sil = liczba*silnia(liczba-1);
  return sil;
}

Musimy być ostrożni przy funkcjach rekurencyjnych, gdyż łatwo za ich pomocą utworzyć funkcję, która będzie sama siebie wywoływała w nieskończoność, a co za tym idzie będzie zawieszała program. Tutaj pierwszymi instrukcjami if ustalamy "warunki stopu", gdzie kończy się wywoływanie funkcji przez samą siebie, a następnie określamy, jak funkcja będzie wywoływać samą siebie (odjęcie jedynki od argumentu, co do którego wiemy, że jest dodatni, gwarantuje, że dojdziemy do warunku stopu w skończonej liczbie kroków).

Warto też zauważyć, że funkcje rekurencyjne czasami mogą być znacznie wolniejsze niż podejście nierekurencyjne (iteracyjne, przy użyciu pętli). Flagowym przykładem może tu być funkcja obliczająca wyrazy ciągu Fibonacciego:

 #include <stdio.h>
 
 unsigned count;
 
 unsigned fib_rec(unsigned n) {
   ++count;
   return n<2 ? n : (fib_rec(n-2) + fib_rec(n-1));
 }
 
 unsigned fib_it (unsigned n) {
   unsigned a = 0, b = 0, c = 1;
   ++count;
   if (!n) return 0;
   while (--n) {
     ++count;
     a = b;
     b = c;
     c = a + b;
   }
   return c;
 }
 
 int main(void) {
   unsigned n, result;
   printf("Ktory element ciagu Fibonacciego obliczyc? ");
   while (scanf("%d", &n)==1) {
     count = 0;
     result = fib_rec(n);
     printf("fib_rec(%3u) = %6u  (wywolan: %5u)\n", n, result, count);
 
     count = 0;
     result = fib_it (n);
     printf("fib_it (%3u) = %6u  (wywolan: %5u)\n", n, result, count);
   }
   return 0;
 }

W tym przypadku funkcja rekurencyjna, choć łatwiejsza w napisaniu, jest bardzo nieefektywna.

Funkcje zagnieżdżone

[edytuj]

W języku C nie można tworzyć zagnieżdżonych funkcji (funkcji wewnątrz innych funkcji).

Ezoteryka C

[edytuj]

C ma wiele niuansów, o których wielu programistów nie wie lub łatwo o nich zapomina:

  • jeśli nie podamy typu wartości zwracanej w funkcji, zostanie przyjęty typ int (według najnowszego standardu C99 nie podanie typu wartości jest zwracane jako błąd);
  • jeśli nie podamy żadnych parametrów funkcji: int funkcja() to funkcja będzie używała zmiennej liczby parametrów (inaczej niż w C++, gdzie przyjęte zostanie, że funkcja nie przyjmuje argumentów). Aby wymusić pustą listę argumentów, należy napisać int funkcja(void) (dotyczy to jedynie prototypów czy deklaracji funkcji);
  • jeśli nie użyjemy w funkcji instrukcji return, wartość zwracana będzie przypadkowa (dostaniemy śmieci z pamięci).
  • W języku C nie jest możliwe przekazywanie typu jako argumentu.

Kompilator C++ użyty do kompilacji kodu C najczęściej zaprotestuje i ostrzeże nas, jeśli użyjemy powyższych konstrukcji. Natomiast czysty kompilator C z domyślnymi ustawieniami nie napisze nic i bez mrugnięcia okiem skompiluje taki kod.

Zobacz też

[edytuj]



Preprocesor

[edytuj]

Wstęp

[edytuj]

Preprocesor jest to program, który analizuje plik źródłowy (programu, biblioteki) w poszukiwaniu wszystkich wyrażeń zaczynających się od "#". Na podstawie tych instrukcji generuje on kod w "czystym" języku C, który następnie jest kompilowany przez kompilator. Ponieważ za pomocą preprocesora można niemal "sterować" kompilatorem, daje on niezwykłe możliwości, które nie były dotąd znane w innych językach programowania.

W języku C wszystkie linijki zaczynające się od symbolu "#" są instrukcjami ( dyrektywami) dla preprocesora. Nie są elementami języka C i nie podlegają bezpośrednio procesowi kompilacji.


Preprocesor może być:

  • standardowy = element kompilatora
  • niestandardowy [29]:
    • CPIP napisany w Pythonie
    • GNU M4
    • Cog
    • Wave parser (Boost)
    • własny program napisany w:

Przełącznik kompilatora

[edytuj]

Aby przekonać się, jak wygląda kod przetworzony przez preprocesor, użyj (w kompilatorze gcc) przełącznika "-E":

gcc test.c -E -o test.txt

W pliku test.txt zostanie umieszczony cały kod w postaci, która zdatna jest do przetworzenia przez kompilator.

Dyrektywy preprocesora

[edytuj]

Dyrektywy preprocesora są to wyrażenia, które zapoczątkowane są symbolem "#" i to właśnie za ich pomocą możemy używać preprocesora. Dyrektywa zaczyna się od znaku # i kończy się wraz z końcem linii. Aby przenieść dalszą część dyrektywy do następnej linii, należy zakończyć linię znakiem "\":

#define ADD(a,b) \
  a+b

Omówimy teraz kilka ważniejszych dyrektyw.

#include

[edytuj]

Najpopularniejsza dyrektywa, wstawiająca w swoje miejsce treść pliku podanego w nawiasach ostrych lub cudzysłowie. Składnia:

Przykład 1 [30]

#include <plik_nagłówkowy_do_dołączenia>

Przykład 2

#include "plik_nagłówkowy_do_dołączenia"

Jeżeli nazwa pliku nagłówkowego będzie ujęta w nawiasy ostre (przykład 1), to kompilator poszuka go wśród własnych plików nagłówkowych (które najczęściej się znajdują w podkatalogu "includes" w katalogu kompilatora). Jeśli jednak nazwa ta będzie ujęta w podwójne cudzysłowy(przykład 2), to kompilator poszuka jej w katalogu, w którym znajduje się kompilowany plik (można zmienić to zachowanie w opcjach niektórych kompilatorów). Przy użyciu tej dyrektywy można także wskazać dokładne położenie plików nagłówkowych poprzez wpisanie bezwzględnej lub względnej ścieżki dostępu do tego pliku nagłówkowego.

Przykład 3 - ścieżka bezwzględna do pliku nagłówkowego w Linuksie i w Windowsie
Opis: W miejsce jednej i drugiej linijki zostanie wczytany plik umieszczony w danej lokalizacji

#include "/usr/include/plik_nagłówkowy.h"
#include "C:\\borland\includes\plik_nagłówkowy.h"


Przykład 4 - ścieżka względna do pliku nagłówkowego
Opis: W miejsce linijki zostanie wczytany plik umieszczony w katalogu "katalog1", a ten katalog jest w katalogu z plikiem źródłowym. Inaczej mówiąc, jeśli plik źródłowy jest w katalogu "/home/user/dokumenty/zrodla", to plik nagłówkowy jest umieszczony w katalogu "/home/user/dokumenty/zrodla/katalog1"

#include "katalog1/plik_naglowkowy.h"


Przykład 5 - ścieżka względna do pliku nagłówkowego
Opis: Jeśli plik źródłowy jest umieszczony w katalogu "/home/user/dokumenty/zrodla", to plik nagłówkowy znajduje się w katalogu "/home/user/dokumenty/katalog1/katalog2/"

#include "../katalog1/katalog2/plik_naglowkowy.h"

Więcej informacji możesz uzyskać w rozdziale Biblioteki.

#define

[edytuj]

Linia pozwalająca zdefiniować:

  • stałą,
  • funkcję
  • słowo kluczowe,
  • makro

które będzie potem podmienione w kodzie programu na odpowiednią wartość lub może zostać użyte w instrukcjach warunkowych dla preprocesora.

Składnia:

#define NAZWA_STALEJ WARTOSC

lub

#define NAZWA_STALEJ

Przykład:
#define LICZBA 8 - spowoduje, że każde wystąpienie słowa LICZBA w kodzie zostanie zastąpione ósemką.
#define SUMA(a,b) ((a)+(b)) - spowoduje, że każde wystąpienie wywołania "funkcji" SUMA zostanie zastąpione przez sumę argumentów

Jeśli w miejscu wartości znajduje się wyrażenie, to należy je umieścić w nawiasach.

#define A  5
#define B  ((2)+(A))

Unikniemy w ten sposób niespodzianek związanych z priorytetem operatorów :

/* 

https://github.com/Keith-S-Thompson/42/blob/master/42.c
This small C program demonstrates the danger of improper use of the C preprocessor.
Keith-S-Thompson


*/ 
#include <stdio.h>

#define SIX 1+5
#define NINE 8+1

int main(void)
{
    printf("%d * %d = %d\n", SIX, NINE, SIX * NINE);
    return 0;

}

Po skompilowaniu i uruchomieniu programu otrzymujemy:

6 * 9 = 42

a powinno być:

6 * 9 = 54

Przyczyną błędu jest interpretacja wyrażenia:

1+5*8+1

Ze względu na brak nawiasów i priorytet operatorów (wyższy * niż +) jest to interpretowane jako:

1+(5*8)+1 

a nie jak:

(1+5)*(8+1)

redefine

[edytuj]

C nie obsługuje żadnych dodatkowych dyrektyw służących do przedefiniowania istniejącego makra.[31]

To samo makro można zdefiniować dowolną ilość razy. Jednakże. spowoduje to zapełnienie stosu ostrzeżeń kompilatora. Dlatego zawsze zaleca się najpierw oddefiniowanie istniejącego makra, a następnie zdefiniowanie go ponownie.

// Define a macro
#define PI 3.14

// Undefine before redefining
#ifdef PI
#undef PI
#endif

// Redefine PI
#define PI 3.14159

#undef

[edytuj]

Ta instrukcja odwołuje definicję wykonaną instrukcją #define.

#undef STALA

instrukcje warunkowe

[edytuj]

Preprocesor zawiera również instrukcje warunkowe, pozwalające na wybór tego co ma zostać skompilowane w zależności od tego, czy stała jest zdefiniowana lub jaką ma wartość:

#if #elif #else #endif

[edytuj]

Te instrukcje uzależniają kompilacje od warunków. Ich działanie jest podobne do instrukcji warunkowych w samym języku C. I tak:

#if
wprowadza warunek, który jeśli nie jest prawdziwy powoduje pominięcie kompilowania kodu, aż do napotkania jednej z poniższych instrukcji.
#else
spowoduje skompilowanie kodu jeżeli warunek za #if jest nieprawdziwy, aż do napotkania któregoś z poniższych instrukcji.
#elif
wprowadza nowy warunek, który będzie sprawdzony jeżeli poprzedni był nieprawdziwy. Stanowi połączenie instrukcji #if i #else.
#endif
zamyka blok ostatniej instrukcji warunkowej.

Przykład:

#if INSTRUKCJE == 2
  printf ("Podaj liczbę z przedziału 10 do 0\n"); /*1*/
#elif INSTRUKCJE == 1
  printf ("Podaj liczbę: "); /*2*/
#else
  printf ("Podaj parametr: "); /*3*/
#endif
scanf ("%d", &liczba); /*4*/
  • wiersz nr 1 zostanie skompilowany jeżeli stała INSTRUKCJE będzie równa 2
  • wiersz nr 2 zostanie skompilowany, gdy INSTRUKCJE będzie równa 1
  • wiersz nr 3 zostanie skompilowany w pozostałych wypadkach
  • wiersz nr 4 będzie kompilowany zawsze

#ifdef #ifndef #else #endif

[edytuj]

Te instrukcje warunkują kompilację od tego, czy odpowiednia stała została zdefiniowana.

#ifdef
spowoduje, że kompilator skompiluje poniższy kod tylko gdy została zdefiniowana odpowiednia stała.
#ifndef
ma odwrotne działanie do #ifdef, a mianowicie brak definicji odpowiedniej stałej umożliwia kompilacje poniższego kodu.
#else,#endif
mają identyczne zastosowanie jak te z powyższej grupy

Przykład:

 #define INFO /*definicja stałej INFO*/
 #ifdef INFO
   printf ("Twórcą tego programu jest Jan Kowalski\n");/*1*/
 #endif
 #ifndef INFO
   printf ("Twórcą tego programu jest znany programista\n");/*2*/
 #endif

To czy dowiemy się kto jest twórcą tego programu zależy czy instrukcja definiująca stałą INFO będzie istnieć. W powyższym przypadku na ekranie powinno się wyświetlić

Twórcą tego programu jest Jan Kowalski

#error

[edytuj]

Powoduje przerwanie kompilacji i wyświetlenie tekstu, który znajduje się za tą instrukcją. Przydatne gdy chcemy zabezpieczyć się przed zdefiniowaniem nieodpowiednich stałych.

Przykład:

#ifdef BLAD
#error Poważny błąd kompilacji
#endif

Co jeżeli zdefiniujemy stałą BLAD, oczywiście przy pomocy dyrektywy #define? Spowoduje to wyświetlenie w trakcie kompilacji komunikatu podobnego do poniższego:

Fatal error program.c 6: Error directive: Poważny błąd kompilacji in function main()
*** 1 errors in Compile ***

wraz z przerwaniem kompilacji.

#warning

[edytuj]

Wyświetla tekst, jako ostrzeżenie. Jest często używany do sygnalizacji programiście, że dana część programu jest przestarzała lub może sprawiać problemy.

Przykład:

#warning To jest bardzo prosty program

Spowoduje to takie oto zachowanie kompilatora:

test.c:3:2: warning: #warning To jest bardzo prosty program

Użycie dyrektywy #warning nie przerywa procesu kompilacji i służy tylko do wyświetlania komunikatów dla programisty w czasie kompilacji programu.

#line

[edytuj]

Powoduje wyzerowanie licznika linii kompilatora, który jest używany przy wyświetlaniu opisu błędów kompilacji. Pozwala to na szybkie znalezienie możliwej przyczyny błędu w rozbudowanym programie.

Przykład:

printf ("Podaj wartość funkcji");
#line
printf ("W przedziale od 10 do 0\n); /* tutaj jest błąd - brak cudzysłowu zamykającego */

Jeżeli teraz nastąpi próba skompilowania tego kodu to kompilator poinformuje, że wystąpił błąd składni w linii 1, a nie np. 258.

#pragma

[edytuj]

Dyrektywa pragma (od angielskiego: pragmatic information) służy do określania i sterowania funkcjami specyficznymi dla kompilatora i docelowej maszyny wykonującej kod. Listę dostępnych dyrektyw #pragma można znaleźć w dokumentacji kompilatora. Służą one m. in. do wymuszania lokalizacji zmiennych i kodu funkcji w pamięci lub tworzenia dodatkowych wątków z użyciem OpenMP.

# oraz ##

[edytuj]

Dość ciekawe możliwości ma w makrach znak "#". Zamienia on stojący za nim identyfikator na napis.

#include <stdio.h>
#define wypisz(x) printf("%s=%i\n", #x, x)
  
int main()
{
  int i=1;
  char a=5;
  wypisz(i);
  wypisz(a);
  return 0;
}

Program wypisze:

i=1
a=5

Czyli wypisz(a) jest rozwijane w printf("%s=%i\n", "a", a).

Natomiast znaki "##" łączą dwie nazwy w jedną. Przykład:

#include <stdio.h>

#define abc(x) int x##_zmienna
#define wypisz(y) printf("%s=%i", #y, y)
int main()
{
  abc(nasza) = 2; // Robi dwie rzeczy:
                  // 1.	Wstawia słowo „nasza” przed słowem „ _zmienna”.
                  // Dzięki temu zadeklarujemy zmienną o nazwie "nasza_zmienna".
                  // 2.	Inicjalizuje „nasza_zmienna” wartością "2".
  wypisz(nasza_zmienna);
  return 0;
}

Program wypisze:

nasza_zmienna=2

Więcej o dobrych zwyczajach w tworzeniu makr można się dowiedzieć w rozdziale Powszechne praktyki.

Makra

[edytuj]

Preprocesor języka C umożliwia też tworzenie makr[32][33], czyli automatycznie wykonywanych czynności.


Makra deklaruje się za pomocą dyrektywy #define:

#define MAKRO(arg1, arg2, ...) (wyrażenie) 
/* można również napisać: do {instrukcje} while(0) */
/* lub jeśli jest tylko jedna instrukcja można napisać: instrukcja (bez średnika!) */

W momencie wystąpienia MAKRA w tekście, preprocesor automatycznie zamieni makro na wyrażenie lub instrukcje. Makra mogą być pewnego rodzaju alternatywami dla funkcji, ale powinno się ich używać tylko w specjalnych przypadkach. Ponieważ makro sprowadza się do prostego zastąpienia przez preprocesor wywołania makra przez jego tekst, jest bardzo podatne na trudne do zlokalizowania błędy (kompilator będzie podawał błędy w miejscach, w których nic nie widzimy - bo preprocesor wstawił tam tekst). Makra są szybsze (nie następuje wywołanie funkcji, które zawsze zajmuje trochę czasu[34]), ale też mniej bezpieczne i elastyczne niż funkcje.

Przeanalizujmy teraz fragment kodu:

#include <stdio.h>
#define KWADRAT(x) ((x)*(x))

int main ()
{
  printf ("2 do kwadratu wynosi %d\n", KWADRAT(2));
  return 0;
}

Preprocesor w miejsce wyrażenia KWADRAT(2) wstawił ((2)*(2)). Zastanówmy się, co stałoby się, gdybyśmy napisali KWADRAT("2"). Preprocesor po prostu wstawi napis do kodu, co da wyrażenie (("2")*("2")), które jest nieprawidłowe. Kompilator zgłosi błąd, ale programista widzi tylko w kodzie użycie makra a nie prawdziwą przyczynę błędu. Widać tu, że bezpieczniejsze jest użycie funkcji, które dają możliwość wyspecyfikowania typów argumentów.

Nawet jeżeli program się skompiluje to makro może dawać nieoczekiwany wynik. Jest tak w przypadku poniższego kodu:

int x = 1;
int y = KWADRAT(++x);

Dzieje się tak dlatego, że makra rozwijane są przez preprocesor i kompilator widzi kod:

int x = 1;
int y = ((++x)*(++x));

Również poniższe makra są błędne[35] pomimo, że opisany problem w nich nie występuje:

#define SUMA(a, b) a + b
#define ILOCZYN(a, b) a * b

Dają one nieoczekiwane wyniki dla wywołań:

SUMA(2, 2) * 2; /* 6 zamiast 8 */
ILOCZYN(2 + 2, 2 + 2); /* 8 zamiast 16 */

Z tego powodu istotne jest użycie nawiasów:

#define SUMA(a, b) ((a) + (b))
#define ILOCZYN(a, b) ((a) * (b))



Predefiniowane makra

[edytuj]

W języku wprowadzono również serię predefiniowanych makr, które mają ułatwić życie programiście. Oto one:

  • __DATE__ - data w momencie kompilacji
  • __TIME__ - godzina w momencie kompilacji
  • __FILE__ - łańcuch, który zawiera nazwę pliku, który aktualnie jest kompilowany przez kompilator
  • __LINE__ - definiuje numer linijki
  • __STDC__ - w kompilatorach zgodnych ze standardem ANSI lub nowszym makro to przyjmuje wartość 1
  • __STDC_VERSION__ - zależnie od poziomu zgodności kompilatora makro przyjmuje różne wartości:
    • jeżeli kompilator jest zgodny z ANSI (rok 1989) makro nie jest zdefiniowane,
    • jeżeli kompilator jest zgodny ze standardem z 1994 makro ma wartość 199409L,
    • jeżeli kompilator jest zgodny ze standardem z 1999 makro ma wartość 199901L.

Warto również wspomnieć o identyfikatorze __func__ zdefiniowanym w standardzie C99, którego wartość to nazwa funkcji.

Spróbujmy użyć tych makr w praktyce:

#include <stdio.h>

#if __STDC_VERSION__ >= 199901L
/*Jezeli mamy do dyspozycji identyfikator __func__ wykorzystajmy go.*/
#define BUG(message) fprintf(stderr, "%s:%d: %s (w funkcji %s)\n", \
                                __FILE__, __LINE__, message, __func__)
#else
/*Jezeli __func__ nie ma, to go nie używamy*/
#define BUG(message) fprintf(stderr, "%s:%d: %s\n", \
                                __FILE__, __LINE__, message)
#endif
 
int main(void) {
  printf("Program ABC,  data kompilacji: %s %s\n", __DATE__, __TIME__);

  BUG("Przykladowy komunikat bledu");
  return 0;
}

Efekt działania programu, gdy kompilowany jest kompilatorem C99:

Program ABC,  data kompilacji: Sep  1 2008 19:12:13
test.c:17: Przykladowy komunikat bledu (w funkcji main)

Gdy kompilowany jest kompilatorem ANSI C:

Program ABC,  data kompilacji: Sep  1 2008 19:13:16
test.c:17: Przykladowy komunikat bledu



pozostałe makra

[edytuj]

Problemy

[edytuj]

Problemy[36]

  • "Połykanie" średnika
  • Błędne zagnieżdżanie
  • Problemy z pierwszeństwem operatorów
  • Duplikacja skutków ubocznych
  • Makra rekurencyjne
  • Pre-skanowanie argumentów
  • Znaki nowej linii w argumentach

Uwagi:

  • NIE umieszczaj znaku średnika na końcu instrukcji #define. To częsty błąd.

Cechy, czyli zalety i wady

[edytuj]

Kiedy używać makr:[37]

  • tylko wtedy, gdy nie masz wyboru i nie możesz użyć funkcji
  • kiedy musisz użyć ich wyniku jako stałej ( uwaga: w c++ lepiej uzyć constexpr )
  • kiedy nie chcesz sprawdzać typu
    • Gdy chcesz zdefiniować funkcję „ogólną” pracującą z kilkoma typami

Kiedy nie używać makr:

  • żeby przyspieszyć kod

Przykłady

[edytuj]
  • CLM_LIBS a collection of code-generating macros for the C preprocessor.




Biblioteka standardowa

[edytuj]

Po co nam biblioteka standardowa?

[edytuj]

W którymś z początkowych rozdziałów tego podręcznika napisane jest, że czysty język C nie może zbyt wiele. Tak naprawdę, to język C sam w sobie praktycznie nie ma mechanizmów do obsługi np. wejścia-wyjścia. Dlatego też większość systemów operacyjnych posiada tzw. bibliotekę standardową zwaną też biblioteką języka C.[38] To właśnie w niej zawarte są podstawowe funkcjonalności, dzięki którym twój program może np. napisać coś na ekranie.

Jak skonstruowana jest biblioteka standardowa?

[edytuj]

Zapytacie zapewne, jak biblioteka standardowa realizuje te funkcje, skoro sam język C tego nie potrafi. Odpowiedź jest prosta - biblioteka standardowa nie jest napisana w samym języku C. Ponieważ C jest językiem tłumaczonym do kodu maszynowego, to w praktyce nie ma żadnych przeszkód, żeby np. połączyć go z językiem niskiego poziomu, jakim jest np. asembler. Dlatego biblioteka C z jednej strony udostępnia gotowe funkcje w języku C, a z drugiej za pomocą niskopoziomowych mechanizmów[39] komunikuje się z systemem operacyjnym, który wykonuje odpowiednie czynności.

Wersje

[edytuj]

Trzy pliki nagłówkowe deiniują funkcje warunkowe, których implementacje nie muszą obsługiwać:

  • complex.h
  • stdatomic.h
  • threads.h

Standard POSIX dodał kilka niestandardowych nagłówków C dla funkcji specyficznych dla systemu Unix, które mogą znaleźć się w innych architekturach

Szereg innych grup używa innych niestandardowych nagłówków – GNU C Library ma alloca.h, a HP OpenVMS ma funkcję va_count().



Jak sprawdzić której wersji używa mój system ?[42]

  • ldd
  • getconf
 ldd --version

przykładowy wynik :

 ldd (Ubuntu GLIBC 2.23-0ubuntu3) 2.23
 Prawa autorskie © 2016 Free Software Foundation, Inc.
 To oprogramowanie jest darmowe; warunki kopiowania są opisane w źródłach.
 Autorzy nie dają ŻADNYCH gwarancji, w tym również gwarancji MOŻLIWOŚCI
 SPRZEDAŻY lub PRZYDATNOŚCI DO KONKRETNYCH ZASTOSOWAŃ.
 Autorami są Roland McGrath i Ulrich Drepper.


 getconf GNU_LIBC_VERSION

Safe libc

[edytuj]

Biblioteka Safe C udostępnia powiązane funkcje sprawdzania pamięci i łańcuchów zgodnie z normą ISO/IEC TR24731. Funkcje te są funkcjami alternatywnymi do istniejącej standardowej biblioteki C, które promują bezpieczniejsze, bezpieczniejsze programowanie. Języki programowania ISO / IEC — specyfikacja C, C11, obejmują teraz ograniczone interfejsy API w dodatku K, „Interfejsy sprawdzania granic”.

Libsafe

Instalacja:

sudo apt install libsafec-dev

musl

[edytuj]
 sudo apt install musl-tools


 git clone git.musl-libc.org/musl


Lokalizacja kompilatora

  • w pliku makefile:
 CC = /usr/bin/musl-gcc

Gdzie są funkcje z biblioteki standardowej?

[edytuj]

Pisząc program w języku C używamy różnego rodzaju funkcji, takich jak np. printf. Nie jesteśmy jednak ich autorami, mało tego nie widzimy nawet deklaracji tych funkcji w naszym programie. Pamiętacie program "Hello world"? Zaczynał on się od takiej oto linijki:

#include <stdio.h>

linijka ta oznacza: "w tym miejscu wstaw zawartość pliku stdio.h". Nawiasy "<" i ">" oznaczają, że plik stdio.h znajduje się w standardowym katalogu z plikami nagłówkowymi. Wszystkie pliki z rozszerzeniem h są właśnie plikami nagłówkowymi. Wróćmy teraz do tematu biblioteki standardowej. Każdy system operacyjny ma za zadanie wykonywać pewne funkcje na rzecz programów. Wszystkie te funkcje zawarte są właśnie w bibliotece standardowej. W systemach z rodziny UNIX nazywa się ją LibC (biblioteka języka C). To tam właśnie znajduje się funkcja printf,[43] scanf, puts i inne.

Oprócz podstawowych funkcji wejścia-wyjścia, biblioteka standardowa udostępnia też możliwość wykonywania:[44]

oraz wykonywania wielu innych rzeczy.


Nazwa Od opis
<assert.h> Zawiera makro assert, które pomaga w wykrywaniu błędów logicznych i innych typów błędów w debugowaniu wersji programu
<complex.h> C99 Zestaw funkcji dla liczb zespolonych
<ctype.h> Zestaw funkcji służących do klasyfikowania znaków według ich typów lub konwersji między dużymi i małymi literami w sposób niezależny od używanego zestawu znaków (zwykle ASCII lub jedno z jego rozszerzeń, chociaż znane są również implementacje wykorzystujące EBCDIC)
<errno.h> Do testowania kodów błędów zgłaszanych przez funkcje biblioteczne
<fenv.h> C99 zestaw funkcji dla liczb zmiennoprzecinkowych
<float.h> Definiuje stałe dla makr określające właściwości biblioteki zmiennoprzecinkowej
<inttypes.h> C99 Defines exact width integer types.
<iso646.h> C95 Defines several macros that implement alternative ways to express several standard tokens. For programming in ISO 646 variant character sets.
<limits.h> Defines macro constants specifying the implementation-specific properties of the integer types.
<locale.h> Defines localization functions
<math.h> Defines common mathematical functions
<setjmp.h> Declares the macros setjmp and longjmp, which are used for non-local exits.
<signal.h> Defines signal handling functions
<stdalign.h> C11 For querying and specifying the alignment of objects.
<stdarg.h> dostęp do zmiennej liczby argumentów przekazywanych do funkcji
<stdatomic.h> C11 For atomic operations on data shared between threads.
<stdbool.h> C99 Defines a boolean data type
<stddef.h> Definiuje kilka przydatnych typów i makr
<stdint.h> C99 Definiuje typy całkowite o dokładnej szerokości, zobacz inttypes.h który zawiera stdint.h i dodaje dodatkowe funkcje[45]
<stdio.h> Defines core input and output functions
<stdlib.h> Defines numeric conversion functions, pseudo-random numbers generation functions, memory allocation, process control functions
<stdnoreturn.h> C11 For specifying non-returning functions.
<string.h> Defines string handling functions.
<tgmath.h> C99 Defines type-generic mathematical functions.
<threads.h> C11 Defines functions for managing multiple Threads as well as mutexes and condition variables.
<time.h> Defines date and time handling functions
<uchar.h> C11 Types and functions for manipulating Unicode characters
<unistd.h> warunkowy
<wchar.h> C95 Definiuje obsługę ciągów szerokich znaków
<wctype.h> C95 Defines set of functions used to classify wide characters by their types or to convert between upper and lower case


Jeśli biblioteka nie jest potrzebna...

[edytuj]

Czasami korzystanie z funkcji bibliotecznych oraz standardowych plików nagłówkowych jest niepożądane np. wtedy, gdy programista pisze swój własny system operacyjny oraz bibliotekę do niego. Aby wyłączyć używanie biblioteki C w opcjach kompilatora GCC możemy dodać następujące argumenty:

-nostdinc -fno-builtin

Opis funkcji biblioteki standardowej

[edytuj]

Podręcznik C na Wikibooks zawiera opis dużej części biblioteki standardowej C:

W systemach uniksowych możesz uzyskać pomoc dzięki narzędziu man, przykładowo pisząc:

man printf

lub

man libc

Uwagi

[edytuj]

Programy w języku C++ mogą dokładnie w ten sam sposób korzystać z biblioteki standardowej, ale zalecane jest, by robić to raczej w trochę odmienny sposób, właściwy dla C++. Szczegóły w podręczniku C++.

Indeksy

[edytuj]
Indeks alfabetyczny
Indeks tematyczny


Zobacz również

[edytuj]
  • glib[46] ( nie glibc, bo bez c na końcu) = GLib (G Library) - niskopoziomowa, narzędziowa biblioteka funkcji dla programistów języka C, dostarczająca jednolite API. GLib jest wykorzystywana przede wszystkim jako podstawa biblioteki GTK+ (której była częścią do wydania wersji 1.1.0) oraz graficznego środowiska GNOME.



Czytanie i pisanie do plików

[edytuj]

Pojęcie pliku

[edytuj]

Na początku dobrze by było, abyś dowiedział się, czym jest plik. Odpowiedni artykuł dostępny jest w Wikipedii. Najprościej mówiąc, plik to pewne dane zapisane na dysku.

Typy metody obsługi plików

[edytuj]

Istnieją dwie metody obsługi czytania i pisania do plików:

Należy pamiętać, że nie wolno nam używać funkcji z obu tych grup jednocześnie w stosunku do jednego, otwartego pliku, tzn. nie można najpierw otworzyć pliku za pomocą fopen(), a następnie odczytywać danych z tego samego pliku za pomocą read().

Czym różnią się oba podejścia do obsługi plików? Otóż metoda wysokopoziomowa ma swój własny bufor, w którym znajdują się dane po odczytaniu z dysku a przed wysłaniem ich do programu użytkownika. W przypadku funkcji niskopoziomowych dane kopiowane są bezpośrednio z pliku do pamięci programu. W praktyce używanie funkcji wysokopoziomowych jest prostsze a przy czytaniu danych małymi porcjami również często szybsze i właśnie ten model zostanie tutaj zaprezentowany.

Identyfikacja pliku

[edytuj]

Każdy z nas, korzystając na co dzień z komputera przyzwyczaił się do tego, że plik ma określoną nazwę. Jednak, w pisaniu programu, posługiwanie się całą nazwą niosłoby ze sobą co najmniej dwa problemy:

  • duże zużycie pamięci - przechowywanie całej nazwy pliku zajmuje niepotrzebnie pamięć,
  • ryzyko błędów (zostały szerzej omówione w rozdziale Napisy).

Programiści korzystają z identyfikatora pliku, który jest pojedynczą liczbą całkowitą. Dzięki temu kod programu jest czytelniejszy i nie trzeba korzystać ciągle z pełnej nazwy pliku. Jednak sam plik nadal jest identyfikowany po swojej nazwie. Aby "przetworzyć" nazwę pliku na odpowiednią liczbę korzystamy z funkcji open lub fopen. Różnica wyjaśniona została poniżej.

Identyfikator pliku

  • w niskopoziomowej metodzie : podstawowym identyfikatorem pliku jest liczba całkowita, która jednoznacznie identyfikuje dany plik w systemie operacyjnym. Liczba ta w systemach typu UNIX jest nazywana deskryptorem pliku.
  • w wysokopoziomowej metodzie: wskaźnik na strukturę typu FILE

Niskopoziomowa obsługa plików

[edytuj]

Niskopoziomowa metoda obsługi plików = obsługa na poziomie deskryptora[47]

Znajdują się tu funkcje do wykonywania niskopoziomowych operacji wejścia/wyjścia na deskryptorach plików[48]

  • prymitywy dla funkcji we/wy wyższego poziomu opisanych w sekcji Wejście/wyjście w strumieniach
  • funkcje do wykonywania operacji sterowania niskiego poziomu, dla których nie ma odpowiedników w strumieniach

Nazwy funkcji są typu read(), open(), write() i close().

We/wy na poziomie strumienia jest bardziej elastyczne i zwykle wygodniejsze; dlatego programiści na ogół używają funkcji na poziomie deskryptora tylko wtedy, gdy jest to konieczne. Oto niektóre z typowych powodów:

  • do odczytu plików binarnych w dużych porcjach
  • do wczytywania całego pliku do rdzenia przed jego analizowaniem.
  • do wykonywania operacji innych niż przesyłanie danych, które można wykonać tylko za pomocą deskryptora. (Możesz użyć fileno, aby uzyskać deskryptor odpowiadający strumieniowi.)
  • Aby przekazać deskryptory do procesu potomnego. (Proces potomny może utworzyć własny strumień, aby używać deskryptora, który dziedziczy, ale nie może bezpośrednio dziedziczyć strumienia)

Podstawowym identyfikatorem pliku jest liczba całkowita, która jednoznacznie identyfikuje dany plik w systemie operacyjnym. Liczba ta w systemach typu UNIX jest nazywana deskryptorem pliku.

Wysokopoziomowa obsługa plików

[edytuj]

Nazwy funkcji z tej grupy zaczynają się od litery "f" (np. fopen(), fread(), fclose()), a identyfikatorem pliku jest wskaźnik na strukturę typu FILE. Owa struktura to pewna grupa zmiennych, która przechowuje dane o pliku - jak na przykład aktualną pozycję w nim. Szczegółami nie musisz się przejmować, funkcje biblioteki standardowej same zajmują się wykorzystaniem struktury FILE. Programista może więc zapomnieć, czym tak naprawdę jest struktura FILE i traktować taką zmienną jako "uchwyt", identyfikator pliku.


Nazwa pliku

[edytuj]

Możemy tworzyć unikalne nazwy plików programowo:[49]

for(i = 0; i < 100; i++) {
    char filename[sizeof "file100.txt"];

    sprintf(filename, "file%03d.txt", i);
    fp = fopen(filename,"w");
}

Formatowanie %03d dopełnia ciąg do 3 cyfr z początkowymi zerami. Dzieki temu pliki będą poprawnie posortowane.

Dane znakowe

[edytuj]

Skupimy się teraz na najprostszym z możliwych zagadnień - zapisie i odczycie pojedynczych znaków oraz całych łańcuchów.

Napiszmy zatem nasz pierwszy program, który stworzy plik "test.txt" i umieści w nim tekst "Hello world":

 #include <stdio.h>
 #include <stdlib.h>
 
 int main ()
 {
   FILE *fp; /* używamy metody wysokopoziomowej - musimy mieć zatem identyfikator pliku, uwaga na gwiazdkę! */
   char tekst[] = "Hello world";
   if ((fp=fopen("test.txt", "w"))==NULL) {
     printf ("Nie mogę otworzyć pliku test.txt do zapisu!\n");
     exit(1);
     }
   fprintf (fp, "%s", tekst); /* zapisz nasz łańcuch w pliku */
   fclose (fp); /* zamknij plik */
   return 0;
 }

Teraz omówimy najważniejsze elementy programu.

  • do identyfikacji pliku używa się wskaźnika na strukturę FILE (czyli FILE *).
  • Funkcja fopen zwraca ów wskaźnik w przypadku poprawnego otwarcia pliku, bądź też NULL, gdy plik nie może zostać otwarty. Pierwszy argument funkcji to nazwa pliku, natomiast drugi to 'tryb dostępu - w oznacza "write" (pisanie). Zwrócony "uchwyt" do pliku będzie mógł być wykorzystany jedynie w funkcjach zapisujących dane. I odwrotnie, gdy otworzymy plik podając tryb r ("read", czytanie), będzie można z niego jedynie czytać dane. Funkcja fopen została dokładniej opisana w odpowiedniej części rozdziału o bibliotece standardowej.


Jak uprościć nazwę typu FILE*? Używając typedef:

typedef FILE* plik;
plik fp;

Po zakończeniu korzystania z pliku należy plik zamknąć. Robi się to za pomocą funkcji fclose. Jeśli zapomnimy o zamknięciu pliku, wszystkie dokonane w nim zmiany zostaną utracone!

Pliki a strumienie

[edytuj]

Można zauważyć, że do zapisu do pliku używamy funkcji fprintf, która wygląda bardzo podobnie do printf - jedyną różnicą jest to, że w fprintf musimy jako pierwszy argument podać identyfikator pliku. Nie jest to przypadek - obie funkcje tak naprawdę robią to samo. Używana do wczytywania danych z klawiatury funkcja scanf też ma swój odpowiednik wśród funkcji operujących na plikach - jak nietrudno zgadnąć, nosi ona nazwę fscanf.

W rzeczywistości język C traktuje tak samo klawiaturę i plik - są to źródła danych, podobnie jak ekran i plik, do których można dane kierować. Jest to myślenie typowe dla systemów typu UNIX, jednak dla użytkowników przyzwyczajonych do systemu Windows albo języków typu Pascal może być to co najmniej dziwne. Nie da się ukryć, że między klawiaturą i plikiem na dysku zachodzą podstawowe różnice i dostęp do nich odbywa się inaczej - jednak funkcje języka C pozwalają nam o tym zapomnieć i same zajmują się szczegółami technicznymi. Z punktu widzenia programisty, urządzenia te sprowadzają się do nadanego im identyfikatora. Uogólnione pliki nazywa się w C strumieniami.

Każdy program w momencie uruchomienia "otrzymuje" od razu trzy otwarte standardowe strumienie ( ang. Standard Streams )[50]:

  • stdin (wejście) = odczytywanie danych wpisywanych przez użytkownika
  • stdout (wyjście) = wyprowadzania informacji dla użytkownika
  • stderr (wyjście błędów) = powiadamiania o błędach

Warunki korzystania ze standardowych strumieni :

  • dołączyć plik nagłówkowy stdio.h
  • nie musimy otwierać ani zamykać strumieni standardowych ( tak jak w przypadku niestandardowych plików : fopen i fclose )



Warto tutaj zauważyć, że konstrukcja:

fprintf (stdout, "Hej, ja działam!");

jest równoważna konstrukcji:

printf ("Hej, ja działam!");

Podobnie jest z funkcją scanf().

fscanf (stdin, "%d", &zmienna);

działa tak samo jak:

scanf("%d", &zmienna);

Obsługa błędów

[edytuj]

Jeśli nastąpił błąd, możemy się dowiedzieć o jego przyczynie na podstawie zmiennej errno zadeklarowanej w pliku nagłówkowym errno.h. Możliwe jest też wydrukowanie komunikatu o błędzie za pomocą funkcji perror. Na przykład używając:

 fp = fopen ("tego pliku nie ma", "r");
 if( fp == NULL )
   {
   perror("błąd otwarcia pliku");
   exit(-10);
   }

dostaniemy komunikat:

błąd otwarcia pliku: No such file or directory

Inny sposób :[51]

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

int main()
{
errno = 0;
FILE *fb = fopen("/home/jeegar/filename","r");
if(fb==NULL)
    printf("its null");
else
    printf("working");


printf("Error %d \n", errno);


}

Zaawansowane operacje

[edytuj]

Pora na kolejny, tym razem bardziej złożony przykład. Oto krótki program, który swoje wejście zapisuje do pliku o nazwie podanej w linii poleceń:

 #include <stdio.h>
 #include <stdlib.h>
 /* program udający bardzo prymitywną wersję programu "tee" */
  
 int main (int argc, char **argv)
 {
    FILE *fp;
    int c;
    if (argc < 2)
    {
        fprintf (stderr, "Uzycie: %s nazwa_pliku\n", argv[0]);
        exit(-1);
    }
    fp = fopen (argv[1], "w");
    if (!fp)
    {
        fprintf (stderr, "Nie moge otworzyc pliku %s\n", argv[1]);
        exit(-1);
    }
    printf("Wcisnij Ctrl+D+Enter lub Ctrl+Z+Enter aby zakonczyc\n");
    while ((c = fgetc(stdin)) != EOF)
    {
        fputc(c, stdout);
        fputc(c, fp);
    }
    fclose(fp);
    return 0;
 }

Tym razem skorzystaliśmy już z dużo większego repertuaru funkcji. Między innymi można zauważyć tutaj funkcję fputc(), która umieszcza pojedynczy znak w pliku. Ponadto w wyżej zaprezentowanym programie została użyta stała EOF, która reprezentuje koniec pliku (ang. End Of File). Powyższy program otwiera plik, którego nazwa przekazywana jest jako pierwszy argument programu, a następnie kopiuje dane z wejścia programu (stdin) na wyjście (stdout) oraz do utworzonego pliku (identyfikowanego za pomocą fp). Program robi to tak długo, aż naciśniemy kombinację klawiszy Ctrl+D (w systemach Unixowych) lub Ctrl+Z(w Windows), która wyśle do programu informację, że skończyliśmy wpisywać dane. Program wyjdzie wtedy z pętli i zamknie utworzony plik.

Rozmiar pliku

[edytuj]

Dzięki standardowym funkcjom języka C możemy m.in. określić długość pliku. Do tego celu służą funkcje fsetpos, fgetpos oraz fseek. Ponieważ przy każdym odczycie/zapisie z/do pliku wskaźnik niejako "przesuwa" się o liczbę przeczytanych/zapisanych bajtów. Możemy jednak ustawić wskaźnik w dowolnie wybranym miejscu. Do tego właśnie służą wyżej wymienione funkcje. Aby odczytać rozmiar pliku powinniśmy ustawić nasz wskaźnik na koniec pliku, po czym odczytać ile bajtów od początku pliku się znajdujemy. Użyjemy do tego tylko dwóch funkcji: fseek oraz fgetpos. Pierwsza służy do ustawiania wskaźnika na odpowiedniej pozycji w pliku, a druga do odczytywania na którym bajcie pliku znajduje się wskaźnik. Kod, który określa rozmiar pliku znajduje się tutaj:

 #include <stdio.h>
 
 int main (int argc, char **argv)
 {
   FILE *fp = NULL;
   fpos_t dlugosc;
   if (argc != 2) {
     printf ("Użycie: %s <nazwa pliku>\n", argv[0]);
     return 1;
     }
   if ((fp=fopen(argv[1], "rb"))==NULL) {
     printf ("Błąd otwarcia pliku: %s!\n", argv[1]);
     return 1;
     }
   fseek (fp, 0, SEEK_END); /* ustawiamy wskaźnik na koniec pliku */
   fgetpos (fp, &dlugosc);
   printf ("Rozmiar pliku: %d\n", dlugosc);
   fclose (fp);
   return 0;
 }

Znajomość rozmiaru pliku przydaje się w wielu różnych sytuacjach, więc dobrze przeanalizuj przykład!

Przykład - pliki graficzne

[edytuj]

Plik graficzny tworzymy :

  • bezpośrednio w C ( fprintf / fwrite )
  • pośrednio za pomocą strumieni i potoku, wtedy :
    • zamiast komend zapisu do pliku ( np. fprintf ) używamy komend wysyłających do standardowego wyjścia ( np. fprint, putchar)[52]
    • zamiast przykładowej komendy : ./a.out używamy : ./a.out > anti.ppm [53]


rastrowy

[edytuj]
dostęp sekwencyjny
[edytuj]
Przykład użycia tej techniki, sekwencyjny dostęp do danych (kod źródłowy)
Przykład użycia tej techniki, swobodny dostęp do danych ( kod źródłowy)

Najprostszym przykładem rastrowego pliku graficznego jest plik PPM. Poniższy program pokazuje jak utworzyć plik w katalogu roboczym programu. Do zapisu:[54]

  • nagłówka pliku używana jest funkcja fprintf, która zapisuje do plików binarnych lub tekstowych
  • tablicy do pliku używana jest funkcja fwrite, która zapisuje do plików binarnych,
 #include <stdio.h>
 int main() {
        const int dimx = 800; 
        const int dimy = 800;
        int i, j;
        FILE * fp = fopen("first.ppm", "wb"); /* b - tryb binarny */
        fprintf(fp, "P6\n%d %d\n255\n", dimx, dimy);
        for(j=0; j<dimy; ++j){
          for(i=0; i<dimx; ++i){         
                        static unsigned char color[3];
                        color[0]=i % 255; /* red */
                        color[1]=j % 255; /* green */
                        color[2]=(i*j) % 255; /* blue */
                        fwrite(color,1,3,fp);
                }
        }
        fclose(fp);
        return 0;
 }

W powyższym przykładzie dostęp do danych jest sekwencyjny.

dostęp swobodny
[edytuj]

Jeśli chcemy mieć swobodny dostęp do danych to :

  • korzystać z funkcji: fsetpos, fgetpos oraz fseek,
  • utworzyć w pamięci tablicę
  • zapisać dane do tablicy
  • przetwarzać tablicę
  • zapisać całą tablicę na dysk w postaci pliku graficznego


Tablica może być:

  • statyczna lub dynamiczna (dla dużych plików dynamiczną)
  • jedno lub wielowymiarowa. Zależy to od
    • koloru : 8-bitowy, 24-bitowy, 32-bitowy, ... )
    • metody tworzenia, usuwanie i dostępu do tablicy


Dostęp ten pozwala na :

  • przetwarzanie danych/obrazów cyfrowych ( ang. digital image processing = DIP), jak : operacje morfologiczne ( ang. Mathematical morphology = MM)
  • przetwarzanie równoległe ( OpenMP, OpenACC, GPU )
  • szybszy ( w pamięci ) dostęp do danych

wektorowy

[edytuj]

Bardzo łatwo również utworzyć plik SVG[55]

/*  

c console program based on :
cpp code by Claudio Rocchini

http://commons.wikimedia.org/wiki/File:Poincare_halfplane_eptagonal_hb.svg


http://validator.w3.org/ 
The uploaded document "circle.svg" was successfully checked as SVG 1.1. 
This means that the resource in question identified itself as "SVG 1.1" 
and that we successfully performed a formal validation using an SGML, HTML5 and/or XML 
Parser(s) (depending on the markup language used). 

*/

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



const double PI = 3.1415926535897932384626433832795;

const int  iXmax = 1000,
           iYmax = 1000,
           radius=100,
           cx=200,
           cy=200;


const char *black="#FFFFFF", /* hexadecimal number as a string for svg color*/
           *white="#000000";
           
 FILE * fp;
 char *filename="circle.svg";
 char *comment = "<!-- sample comment in SVG file  \n can be multi-line -->";




void draw_circle(FILE * FileP,int radius,int cx,int cy)
{
    fprintf(FileP,"<circle cx=\"%d\" cy=\"%d\" r=\"%d\" style=\"stroke:%s; stroke-width:2; fill:%s\"/>\n",
    cx,cy,radius,white,black);
}


int main(){
    
        // setup
        fp = fopen(filename,"w");
	fprintf(fp,
		    "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
		    "%s \n "
           "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \n"
           "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"
           "<svg width=\"20cm\" height=\"20cm\" viewBox=\"0 0 %d %d \"\n"
           " xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">\n",
           comment,iXmax,iYmax);

        // draw
	draw_circle(fp,radius,cx,cy);
	

	
    
        // end 
        fprintf(fp,"</svg>\n");
	fclose(fp);
	printf(" file %s saved \n",filename ); 
	
	return 0;
}

Przykłady

[edytuj]

Co z katalogami?

[edytuj]

Faktycznie, zapomnieliśmy o nich. Jednak wynika to z tego, że specyfikacja ANSI C nie uwzględnia obsługi katalogów. Dlatego też aby dowiedzieć się więcej o obsłudze katalogów w języku C zapraszamy do podręcznika o programowaniu w systemie UNIX.



Ćwiczenia

[edytuj]

Ćwiczenia

[edytuj]

Wszystkie, zamieszczone tutaj ćwiczenia mają na celu pomóc Ci w sprawdzeniu Twojej wiedzy oraz umożliwieniu Tobie wykorzystania nowo nabytych wiadomości w praktyce. Pamiętaj także, że ten podręcznik ma służyć także innym, więc nie zamieszczaj tutaj Twoich rozwiązań. Zachowaj je dla siebie.

Ćwiczenie 1

[edytuj]

Napisz program, który wyświetli twoje imię i nazwisko.

Ćwiczenie 2

[edytuj]

Napisz program, który poprosi o podanie dwóch liczb rzeczywistych i wyświetli wynik mnożenia obu zmiennych.

Ćwiczenie 3

[edytuj]

Napisz program, który pobierze jako argumenty z linii komend nazwy dwóch plików i przekopiuje zawartość pierwszego pliku do drugiego (tworząc lub zamazując drugi).

Ćwiczenie 4

[edytuj]

Napisz program, który utworzy nowy plik (o dowolnie wybranej przez Ciebie nazwie) i zapisze tam:

  1. Twoje imię
  2. wiek
  3. miasto, w którym mieszkasz

Przykładowy plik powinien wyglądać tak:

Stanisław
30
Kraków

Ćwiczenie 5

[edytuj]

Napisz program generujący tabliczkę mnożenia 10 x 10 i wyświetlający ją na ekranie.

Ćwiczenie 6 - dla chętnych

[edytuj]

Napisz program znajdujący pierwiastki trójmianu kwadratowego ax2+bx+c=0, dla zadanych parametrów a, b, c.


Tablice

[edytuj]

W rozdziale Zmienne w C dowiedziałeś się, jak przechowywać pojedyncze liczby oraz znaki. Czasami zdarza się jednak, że potrzebujemy przechować kilka, kilkanaście albo i więcej zmiennych jednego typu. Nie tworzymy wtedy np. dwudziestu osobnych zmiennych. W takich przypadkach z pomocą przychodzi nam tablica.

Tablica to ciąg zmiennych jednego typu. Ciąg taki posiada jedną nazwę a do jego poszczególnych elementów odnosimy się przez numer (indeks).

tablica 10-elementowa



Wstęp

[edytuj]

Typy tablic

[edytuj]

Podział wg sposobu definiowania:

  • statyczne
  • dynamiczne
    • tablice, których rozmiar jest definiowany przez zmienną ( ang. Variable-length array)[56][57]
    • tablice, których rozmiar jest niezdefiniowany. Tablice te są elementem struktury.(ang. Flexible array member)[58]


Podział wg typu elementu

  • tablice znaków
  • tablice liczb całkowitych


Podział wg rozmiaru

  • jednowymiarowe
  • wielowymiarowe

Tablice znaków

[edytuj]

Tablice znaków, tj. typu char oraz unsigned char, posiadają dwie ogólnie przyjęte nazwy, zależnie od ich przeznaczenia:

  • bufory - gdy wykorzystujemy je do przechowywania ogólnie pojętych danych, gdy traktujemy je jako po prostu "ciągi bajtów" (typ char ma rozmiar 1 bajta, więc jest elastyczny do przechowywania np. danych wczytanych z pliku przed ich przetworzeniem).
  • napisy - gdy zawarte w nich dane traktujemy jako ciągi liter; jest im poświęcony osobny rozdział Napisy.

Przykład:

/*
 http://joequery.me/code/snprintf-c/
 
 
  gcc a.c -Wall
 ./a.out
 
012345678
hello th\0
turtle\078
2222\05678

*/ 
#include<stdio.h>
#define BUFSIZE 9




void init_buf(char *buf, size_t size){
    int i;
    for(i=0; i<size; i++){
        buf[i] = i + '0'; // int to char conversion
    }
}

void print_buf(char *buf){
    int i;
    char c;
    for(i=0; i<BUFSIZE; i++){
        c = buf[i];
        if(c == '\0'){
            printf("\\0");

        }
        else{
            printf("%c", buf[i]);
        }
    }
    printf("\n");
}


int main(){
    char buf[BUFSIZE];
    init_buf(buf, BUFSIZE);
    print_buf(buf);

    // hello there! == 12 characters, > BUFSIZE
    init_buf(buf, BUFSIZE);
    snprintf(buf, BUFSIZE, "hello there!");
    print_buf(buf);

    // turtle == 6 charaters, < BUFSIZE
    init_buf(buf, BUFSIZE);
    snprintf(buf, BUFSIZE, "turtle");
    print_buf(buf);

    // 2222220 == 7 charaters, > 5
    init_buf(buf, BUFSIZE);
    snprintf(buf, 5, "%d", 222222 * 10);
    print_buf(buf);

    return 0;
}

Tablice wielowymiarowe

[edytuj]
tablica dwuwymiarowa (5x5)

Rozważmy teraz konieczność przechowania w pamięci komputera całej macierzy o wymiarach 10 x 10. Można by tego dokonać tworząc 10 osobnych tablic jednowymiarowych, reprezentujących poszczególne wiersze macierzy. Jednak język C dostarcza nam dużo wygodniejszej metody, która w dodatku jest bardzo łatwa w użyciu. Są to tablice wielowymiarowe[59], lub inaczej "tablice tablic". Tablice wielowymiarowe definiujemy podając przy zmiennej kilka wymiarów, np.:

 float macierz[10][10];

Tak samo wygląda dostęp do poszczególnych elementów tablicy:

 macierz[2][3] = 1.2;

Jak widać ten sposób jest dużo wygodniejszy (i zapewne dużo bardziej "naturalny") niż deklarowanie 10 osobnych tablic jednowymiarowych. Aby zainicjować tablicę wielowymiarową należy zastosować zagłębianie klamr, np.:

 float macierz[3][4] = {
   { 1.6, 4.5, 2.4, 5.6 },  /* pierwszy wiersz */
   { 5.7, 4.3, 3.6, 4.3 },  /* drugi wiersz */
   { 8.8, 7.5, 4.3, 8.6 }   /* trzeci wiersz */
 };

Dodatkowo, pierwszego wymiaru nie musimy określać (podobnie jak dla tablic jednowymiarowych) i wówczas kompilator sam ustali odpowiednią wielkość, np.:

 float macierz[][4] = {
   { 1.6, 4.5, 2.4, 5.6 },  /* pierwszy wiersz */
   { 5.7, 4.3, 3.6, 4.3 },  /* drugi wiersz */
   { 8.8, 7.5, 4.3, 8.6 },  /* trzeci wiersz */
   { 6.3, 2.7, 5.7, 2.7 }  /* czwarty wiersz */
 };

Innym, bardziej elastycznym sposobem deklarowania tablic wielowymiarowych, jest użycie wskaźników. Opisane to zostało w następnym rozdziale.


Kolejność głównych wierszy

[edytuj]

Kolejność głównych wierszy ( ang. Row Major Order = ROM [60])

W C tablica wielowymiarowa A[n][m] :

  • jest przechowywana wierszami[61] :
  • numeracja indeksów rozpoczyna się od zera
 A[0][0], A[0][1], ..., A[0][m-1], A[1][0], A[1][1],..., A[n-1][m-1] 
C: row-major order (lexicographical access order), zero-based indexing
Address
x + N_x*y
Access
A[y][x]
Value
0 A[0][0]
1 A[0][1]
2 A[0][2]
3 A[1][0]
4 A[1][1]
5 A[1][2]


Przykładowy program :

/*
 http://stackoverflow.com/questions/2151084/map-a-2d-array-onto-a-1d-array-c/2151113

*/
  #include <stdio.h>

   int main(int argc, char **argv) {
   int i, j, k;
   int arr[5][3];
   int *arr2 = (int*)arr;

       for (k=0; k<15; k++) {
          arr2[k] = k;
          printf("arr[%d] = %2d\n", k, arr2[k]);
       }

       for (i=0; i<5; i++) {
         for (j=0; j< 3; j++) {
            printf("arr2[%d][%d] = %2d\n", i, j ,arr[i][j]);
         }
       } 
    }


Działania na tablicach

[edytuj]

Sposoby deklaracji tablic

[edytuj]

Tablicę deklaruje się w następujący sposób:

 typ nazwa_tablicy[rozmiar];

gdzie rozmiar oznacza ile zmiennych danego typu możemy zmieścić w tablicy. Zatem aby np. zadeklarować tablicę, mieszczącą 20 liczb całkowitych możemy napisać tak:

 int tablica[20];

Podobnie jak przy deklaracji zmiennych, także tablicy możemy nadać wartości początkowe przy jej deklaracji. Odbywa się to przez umieszczenie wartości kolejnych elementów oddzielonych przecinkami wewnątrz nawiasów klamrowych:

 int tablica[3] = {0,1,2};

Niekoniecznie trzeba podawać rozmiar tablicy, np.:

 int tablica[] = {1, 2, 3, 4, 5};

W takim przypadku kompilator sam ustali rozmiar tablicy (w tym przypadku - 5 elementów).

Rozpatrzmy następujący kod:

 #include <stdio.h>
 #define ROZMIAR 3
 int main()
 {
   int tab[ROZMIAR] = {3,6,8};
   int i;
   puts ("Druk tablicy tab:");
 
   for (i=0; i<ROZMIAR; ++i) {
     printf ("Element numer %d = %d\n", i, tab[i]);
   }
   return 0;
 }

Wynik:

Druk tablicy tab:
Element numer 0 = 3
Element numer 1 = 6
Element numer 2 = 8

Jak widać, wszystko się zgadza.

W powyżej zamieszczonym przykładzie użyliśmy stałej do podania rozmiaru tablicy. Jest to o tyle pożądany zwyczaj, że w razie potrzeby zmiany rozmiaru tablicy, zmieniana jest tylko wartość w jednej linijce kodu przy #define, w innym przypadku musielibyśmy szukać wszystkich wystąpień rozmiaru rozsianych po kodzie całego programu.

Odczyt/zapis wartości do tablicy

[edytuj]

Tablicami posługujemy się tak samo jak zwykłymi zmiennymi. Różnica polega jedynie na podawaniu indeksu tablicy. Określa on, z którego elementu (wartości) chcemy skorzystać spośród wszystkich umieszczonych w tablicy. Numeracja indeksów rozpoczyna się od zera, co oznacza, że pierwszy element tablicy ma indeks równy 0, drugi 1, trzeci 2, itd.

Spróbujmy przedstawić to na działającym przykładzie. Przeanalizuj następujący kod:

 int tablica[5] = {0};
 int i = 0;
 tablica[2] = 3;
 tablica[3] = 7;
 for (i=0;i!=5;++i) {
   printf ("tablica[%d]=%d\n", i, tablica[i]);
 }

Jak widać, na początku deklarujemy 5-elementową tablicę, którą od razu zerujemy. Następnie pod trzeci i czwarty element (liczone począwszy od 0) podstawiamy liczby 3 i 7. Pętla ma za zadanie wyprowadzić wynik naszych działań.

Tablica może być również zmieniana w obrębie funkcji

rozmiar tablicy

[edytuj]

Rozmiar tablicy jednowymiarowej[62]

   size_t n = sizeof( a ) / sizeof( a[0] );

Zobacz :

  • gcc Wsizeof-array-argument

Ograniczenia tablic

[edytuj]

Pomimo swej wygody tablice statyczne mają ograniczony, z góry zdefiniowany rozmiar, którego nie można zmienić w trakcie działania programu. Dlatego też w niektórych zastosowaniach tablice statyczne zostały wyparte tablicami dynamicznymi, których rozmiar może być określony w trakcie działania programu. Zagadnienie to zostało opisane w następnym rozdziale.

Wystarczy pomylić się o jedno miejsce (tzw. błąd off by one) by spowodować, że działanie programu zostanie nagle przerwane przez system operacyjny ( błąd przy uruchamianiu ) :

 int foo[100];
 int i;
 
 for (i=0; i<=100; i+=1) /* powinno być i<100 */
   foo[i] = 0;

 /* program powinien zakończyć się błędem */


Rozwiązanie:

  • cppcheck sprawdza zakres (Bounds checking )

Zobacz również

[edytuj]
  • Więcej o tablicach (rozszerzenie materiału)
  • Tablice jako parameter funkcji
  • tablice dynamiczne
  • zfp to biblioteka C/C++ typu open source dla skompresowanych tablic numerycznych, która obsługuje dostęp swobodny do odczytu i zapisu o dużej przepustowości. zfp obsługuje również kompresję strumieniową danych całkowitych i zmiennoprzecinkowych, np. dla aplikacji, które odczytują i zapisują duże zbiory danych na dysku iz niego. zfp jest napisany głównie w C i C++, ale zawiera również powiązania Python i Fortran.
  • BitArray in C by Isaac Turner


Wskaźniki

[edytuj]

Zmienne w komputerze są przechowywane w pamięci. To wie każdy programista, a dobry programista potrafi kontrolować zachowanie komputera w przydzielaniu i obsłudze pamięci dla zmiennych. W tym celu pomocne są wskaźniki.

Co to jest wskaźnik?

[edytuj]
Wskaźnik a wskazujący na zmienną b. Zauważmy, że b przechowuje liczbę, podczas gdy a przechowuje adres b w pamięci (1462)

Wskaźnik (ang. pointer)[65] to specjalny rodzaj zmiennej, w której zapisany jest adres w pamięci komputera. Oznacza to, że wskaźnik wskazuje miejsce, gdzie zapisana jest jakaś informacja (np. zmienna typu liczbowego czy struktura).

Obrazowo możemy wyobrazić sobie pamięć komputera jako bibliotekę a zmienne jako książki. Zamiast brać książkę z półki samemu (analogicznie do korzystania wprost ze zwykłych zmiennych), możemy podać bibliotekarzowi wypisany rewers z numerem katalogowym książki a on znajdzie ją za nas. Analogia ta nie jest doskonała, ale pozwala wyobrazić sobie niektóre cechy wskaźników: numer na rewersie identyfikuje pewną książkę, kilka rewersów może dotyczyć tej samej książki, numer w rewersie możemy skreślić i użyć go do zamówienia innej książki, a jeśli wpiszemy nieprawidłowy numer, to możemy dostać nie tę książkę, którą chcemy, albo też nie dostać nic.

Warto też przytoczyć w tym miejscu definicję adresu pamięci. Możemy powiedzieć, że adres to pewna liczba całkowita, jednoznacznie definiująca położenie pewnego obiektu w pamięci komputera. Tymi obiektami mogą być np. zmienne, elementy tablic czy nawet funkcje. Dokładniejszą definicję możesz znaleźć w Wikipedii.



Podstawy wskaźników [66]
symbol znaczenie użycie
* weź wartość x *x
* deklaracja wskaźnika do wartości int *x;
& weź adres &x

Kiedy korzystać ze wskaźników ?

[edytuj]

Jak dokumentować użycie wskaźników ?

[edytuj]

Grafika Unicode [67]


  ┌─head─┐     ┌──value──┐       ┌──value──┐
  │   ├───────►│    4    │       │    0    │
  └──────┘     ├───next──┤       ├───next──┤
               │    ├───────────►│   NULL  │
               └─────────┘       └─────────┘


Operowanie na wskaźnikach

[edytuj]

By stworzyć wskaźnik do zmiennej i móc się nim posługiwać, należy przypisać mu odpowiednią wartość - adres obiektu, na jaki chcieliśmy aby wskazywał. Skąd mamy znać ten adres? W języku C możemy "zapytać się" o adres za pomocą operatora & (operatora pobrania adresu). Przeanalizuj następujący kod:

 #include <stdio.h>
 
 int main (void)
 {
   int liczba = 80;
   printf("Wartość zmiennej liczba: %d\n", liczba );
   printf("Adres zmiennej liczba: %p\n", &liczba );
   return 0;
 }

Program ten wypisuje adres pamięci, pod którym znajduje się zmienna oraz wartość jaką kryje zmienna przechowywana pod owym adresem. Przykładowy wynik:

Wartość zmiennej liczba: 80
Adres zmiennej liczba: 0022FF74

Aby móc przechowywać taki adres, zadeklarujemy zmienną wskaźnikową. Ważną informacją, oprócz samego adresu wskazywanej zmiennej, jest typ wskazywanej zmiennej. Mimo że wskaźnik jest zawsze typu adresowego, kompilator wymaga od nas, abyśmy przy deklaracji podali typ zmiennej, na którą wskaźnik będzie wskazywał. Robi się to poprzez dodanie * (gwiazdki) przed nazwą wskaźnika, np.:

 int *wskaznik1;    // zmienna wskaźnikowa na obiekt typu liczba całkowita
 char *wskaznik2;    // zmienna wskaźnikowa na obiekt typu znak
 float *wskaznik3;    // zmienna wskaźnikowa na obiekt typu liczba zmiennoprzecinkowa

Niektórzy programiści mogą nieco błędnie interpretować wskaźnik do typu jako nowy typ i uważać, że jeśli napiszą:

 int * a,b,c;

to otrzymają trzy wskaźniki do liczby całkowitej. W rzeczywistości uzyskamy jednak tylko jeden wskaźnik a, oraz dwie liczby całkowite b i c (tak jakbyśmy napisali int *a; int b, int c). W tym przypadku trzy wskaźniki otrzymamy pisząc:

 int *a,*b,*c;

Aby uniknąć pomyłek, lepiej jest pisać gwiazdkę tuż przy zmiennej, albo jeszcze lepiej - nie mieszać deklaracji wskaźników i zmiennych:

 int *a;
 int b,c;

Dostęp do wskazywanego obiektu

[edytuj]

Aby dobrać się do wartości wskazywanej przez wskaźnik, należy użyć unarnego operatora * (gwiazdka), zwanego operatorem wyłuskania. Mimo, że kolejny raz używamy gwiazdki, oznacza ona teraz coś zupełnie innego. Jest tak, ponieważ używamy jej w zupełnie innym miejscu: nie przy deklaracji zmiennej (gdzie gwiazdka oznacza deklarowanie wskaźnika), a przy wykorzystaniu zmiennej, gdzie odgrywa rolę operatora, podobnie jak operator & (pobrania adresu obiektu). Program ilustrujący:

 #include <stdio.h>
 
 int main (void)
 {
   int liczba = 80;
   int *wskaznik = &liczba;   // wskaznik przechowuje adres, ktory pobieramy od zmiennej liczba

   printf("Wartosc zmiennej: %d, jej adres: %p.\n", liczba, &liczba);
   printf("Adres przechowywany we wskazniku: %p, wskazywana wartosc: %d.\n",
          wskaznik, *wskaznik);
 
   *wskaznik = 42;   // zapisanie liczby 42 do obiektu, na który wskazuje wskaznik
   printf("Wartosc zmiennej: %d, wartosc wskazywana przez wskaznik: %d\n",
          liczba, *wskaznik);
 
   liczba = 0x42;  // liczba podana w systemie szesnastkowym, podana po prefiksie 0x
   printf("Wartosc zmiennej: %d, wartosc wskazywana przez wskaznik: %d\n",
          liczba, *wskaznik);
 
   return 0;
 }

Przykładowy wynik programu:

Wartosc zmiennej: 80, jej adres: 0022FF74.
Adres przechowywany we wskazniku: 0022FF74, wskazywana wartosc: 80.
Wartosc zmiennej: 42, wartosc wskazywana przez wskaznik: 42
Wartosc zmiennej: 66, wartosc wskazywana przez wskaznik: 66

Gdy argument jest wskaźnikiem...

[edytuj]

Czasami zdarza się, że argumentami funkcji są wskaźniki. W przypadku zwykłych zmiennych, nasza funkcja otrzymuje jedynie lokalne kopie argumentów, które zostały jej podane. Wszelkie zmiany dokonują się lokalnie i nie są widziane poza funkcją. Przekazując do funkcji wskaźnik, również zostaje stworzona kopia... wskaźnika, na którym możemy operować. Tu jednak kopiowanie i niewidoczne lokalne zmiany się kończą. Obiekt, na który wskazuje ten wskaźnik, znajduje się gdzieś w pamięci i możemy na nim działać (czyli na oryginale), tak więc zmiany te są widoczne po wyjściu z funkcji. Spróbujmy rozpatrzyć poniższy przykład:

 #include <stdio.h>
 
 void func_var (int zmienna)
 {
   zmienna = 4;
 }
 void func_pointer (int *zmienna)
 {
   (*zmienna) = 5;
 }

 int main (void)
 {
   int z=3;
   printf ("z= %d\n", z);

   func_var (z);
   printf ("z= %d\n", z);

   func_pointer (&z);
   printf ("z= %d\n", z);

   return 0;
 }

Wynikiem będzie:

z= 3
z= 3
z= 5

Widzimy, że funkcje w języku C nie tylko potrafią zwracać określoną wartość, lecz także zmieniać dane, podane im jako argumenty. Ten sposób przekazywania argumentów do funkcji jest nazywany przekazywaniem przez wskaźnik (w przeciwieństwie do normalnego przekazywania przez wartość).

Pułapki wskaźników

[edytuj]

Ważne jest, aby przy posługiwaniu się wskaźnikami nigdy nie próbować odwoływać się do komórki wskazywanej przez wskaźnik o wartości NULL ani nie używać niezainicjowanego wskaźnika! Przykładem nieprawidłowego kodu może być np.:

 int *wsk;
 printf ("zawartosc komorki: %d\n", *(wsk));   /* Błąd */
 wsk = NULL;
 printf ("zawartosc komorki: %d\n", *(wsk));   /* Błąd */

Pamiętaj też, że możesz być rozczarowany używając operatora sizeof, podając zmienną wskaźnikową. Uzyskana wielkość będzie oznaczała rozmiar adresu, a nie rozmiar typu użytego podczas deklarowania naszego wskaźnika. Wielkość ta będzie zawsze miała taki sam rozmiar dla każdego wskaźnika, w zależności od kompilatora, a także docelowej platformy. Zamiast tego używaj: sizeof(*wskaźnik). Przykład:

  char *zmienna;
  int z = sizeof zmienna; /* z może być równe 4 (rozmiar adresu na maszynie 32bit) */
  z = sizeof(char*);      /* robimy to samo, co wyżej */
  z = sizeof *zmienna;    /* tym razem z= rozmiar znaku, tj. 1 */
  z = sizeof(char);       /* robimy to samo, co wyżej */

Stałe wskaźniki

[edytuj]

Podobnie jak możemy deklarować zwykłe stałe, tak samo możemy mieć stałe wskaźniki - jednak są ich dwa rodzaje. Wskaźniki na stałą wartość:

 const int *a;
 int const * a;  /* równoważnie */

oraz stałe wskaźniki:

 int * const b;

Słówko const przed typem działa jak w przypadku zwykłych stałych, tzn. nie możemy zmienić wartości wskazywanej przy pomocy wskaźnika.

W drugim przypadku słowo const jest tuż za gwiazdką oznaczającą typ wskaźnikowy, co skutkuje stworzeniem stałego wskaźnika, czyli takiego którego nie można przestawić na inny adres.

Obie opcje można połączyć, deklarując stały wskaźnik, którym nie można zmienić wartości wskazywanej zmiennej, i również można zrobić to na dwa sposoby:

 const int * const c;
 int const * const c;  /* równoważnie  */

 int i=0;
 const int *a=&i;
 int * const b=&i;
 int const * const c=&i;
 *a = 1;  /* kompilator zaprotestuje */
 *b = 2;  /* ok */
 *c = 3;   /* kompilator zaprotestuje */
 a = b;   /* ok */
 b = a;   /* kompilator zaprotestuje */
 c = a;   /* kompilator zaprotestuje */

Wskaźniki na stałą wartość są przydatne między innymi w sytuacji gdy mamy duży obiekt (na przykład strukturę z kilkoma polami). Jeśli przypiszemy taką zmienną do innej zmiennej, kopiowanie może potrwać dużo czasu, a oprócz tego zostanie zajęte dużo pamięci. Przekazanie takiej struktury do funkcji albo zwrócenie jej jako wartość funkcji wiąże się z takim samym narzutem. W takim wypadku dobrze jest użyć wskaźnika na stałą wartość.

 void funkcja(const duza_struktura *ds)
 {
    /* czytamy z ds i wykonujemy obliczenia */
 }
 ....
 funkcja(&dane); /* mamy pewność, że zmienna dane nie zostanie zmieniona */

Dynamiczna alokacja pamięci - tablice dynamiczne

[edytuj]

Mając styczność z tablicami można się zastanowić, czy nie dałoby się mieć tablic, których rozmiar dostosowuje się do naszych potrzeb a nie jest na stałe zaszyty w kodzie programu. Chcąc pomieścić więcej danych możemy po prostu zwiększyć rozmiar tablicy - ale gdy do przechowania będzie mniej elementów okaże się, że marnujemy pamięć. Język C umożliwia dzięki wskaźnikom i dynamicznej alokacji pamięci tworzenie tablic takiej wielkości, jakiej akurat potrzebujemy.

O co chodzi

[edytuj]
Pamięć w C

Czym jest dynamiczna alokacja pamięci? Normalnie zmienne programu przechowywane są na tzw. stosie (ang. stack) - powstają, gdy program wchodzi do bloku, w którym zmienne są zadeklarowane a zwalniane w momencie, kiedy program opuszcza ten blok. Jeśli deklarujemy tak tablice, to ich rozmiar musi być znany w momencie kompilacji - żeby kompilator wygenerował kod rezerwujący odpowiednią ilość pamięci.

Dostępny jest jednak drugi rodzaj rezerwacji (czyli alokacji) pamięci. Jest to alokacja na stercie (ang. heap). Sterta to obszar pamięci wspólny dla całego programu, przechowywane są w nim zmienne, których czas życia nie jest związany z poszczególnymi blokami. Musimy sami rezerwować dla nich miejsce i to miejsce zwalniać, ale dzięki temu możemy to zrobić w dowolnym momencie działania programu.

Należy pamiętać, że rezerwowanie i zwalnianie pamięci na stercie zajmuje więcej czasu niż analogiczne działania na stosie. Dodatkowo, zmienna zajmuje na stercie więcej miejsca niż na stosie - sterta utrzymuje specjalną strukturę, w której trzymane są wolne partie (może to być np. lista). Tak więc używajmy dynamicznej alokacji tam, gdzie jest potrzebna - dla danych, których rozmiaru nie jesteśmy w stanie przewidzieć na etapie kompilacji lub ich żywotność ma być niezwiązana z blokiem, w którym zostały zaalokowane.

Obsługa pamięci

[edytuj]

Podstawową funkcją do rezerwacji pamięci jest funkcja malloc. Jest to niezbyt skomplikowana funkcja - podając jej rozmiar (w bajtach) potrzebnej pamięci, dostajemy wskaźnik do zaalokowanego obszaru.

Załóżmy, że chcemy stworzyć tablicę liczb typu float:

 //Pamietaj aby dodac na poczatku biblioteke stdlib.h!

 int rozmiar;
 float *tablica;
 
 rozmiar = 3;
 tablica = (float*) malloc(rozmiar * sizeof(*tablica)); //pierwsza gwiazdka (*) w funkcji malloc() to operator mnozenia
 tablica[0] = 0.1;

Przeanalizujmy teraz po kolei, co dzieje się w powyższym fragmencie. Najpierw deklarujemy zmienne - rozmiar tablicy i wskaźnik, który będzie wskazywał obszar w pamięci, gdzie będzie trzymana tablica. Do zmiennej "rozmiar" możemy w trakcie działania programu przypisać cokolwiek - wczytać ją z pliku, z klawiatury, obliczyć, wylosować - nie jest to istotne. rozmiar * sizeof(*tablica) oblicza potrzebną wielkość tablicy. Dla każdej zmiennej float potrzebujemy tyle bajtów, ile zajmuje ten typ danych. Ponieważ może się to różnić na rozmaitych maszynach, istnieje operator sizeof, zwracający dla danego wyrażenia rozmiar jego typu w bajtach.

W wielu książkach (również K&Rv2) i w Internecie stosuje się inny schemat użycia funkcji malloc a mianowicie: tablica = (float*)malloc(rozmiar * sizeof(float)). Takie użycie należy traktować jako błędne, gdyż nie sprzyja ono poprawnemu wykrywaniu błędów.

Rozważmy sytuację, gdy programista zapomni dodać plik nagłówkowy stdlib.h, wówczas kompilator (z braku deklaracji funkcji malloc) przyjmie, że zwraca ona typ int, zatem do zmiennej tablica (która jest wskaźnikiem) będzie przypisywana liczba całkowita, co od razu spowoduje błąd kompilacji (a przynajmniej ostrzeżenie), dzięki czemu będzie można szybko poprawić kod programu. Rzutowanie jest konieczne tylko w języku C++, gdzie konwersja z void* na inne typy wskaźnikowe nie jest domyślna, ale język ten oferuje nowe sposoby alokacji pamięci.

Teraz rozważmy sytuację, gdy zdecydujemy się zwiększyć dokładność obliczeń i zamiast typu float użyć typu double. Będziemy musieli wyszukać wszystkie wywołania funkcji malloc, calloc i realloc odnoszące się do naszej tablicy i zmieniać wszędzie sizeof(float) na sizeof(double). Aby temu zapobiec lepiej od razu użyć sizeof(*tablica), wówczas zmiana typu zmiennej tablica na double* zostanie od razu uwzględniona przy alokacji pamięci.

Dodatkowo, należy sprawdzić, czy funkcja malloc nie zwróciła wartości NULL - dzieje się tak, gdy zabrakło pamięci. Ale uwaga: może się tak stać również jeżeli jako argument funkcji podano zero.

Jeśli dany obszar pamięci nie będzie już nam więcej potrzebny powinniśmy go zwolnić, aby system operacyjny mógł go przydzielić innym potrzebującym procesom.


Do zwolnienia obszaru pamięci używamy funkcji free(), która przyjmuje tylko jeden argument - wskaźnik, który otrzymaliśmy w wyniku działania funkcji malloc().

 free (tablica);

Należy też uważać, by nie zwalniać dwa razy tego samego miejsca. Po wywołaniu free wskaźnik nie zmienia wartości, pamięć wskazywana przez niego może też nie od razu ulec zmianie. Czasem możemy więc korzystać ze wskaźnika (zwłaszcza czytać) po wywołaniu free nie orientując się, że robimy coś źle - i w pewnym momencie dostać komunikat o nieprawidłowym dostępie do pamięci. Z tego powodu zaraz po wywołaniu funkcji free można przypisać wskaźnikowi wartość 0.

Czasami możemy potrzebować zmienić rozmiar już przydzielonego bloku pamięci. Tu z pomocą przychodzi funkcja realloc:

 tablica = realloc(tablica, 2*rozmiar*sizeof(*tablica));

Funkcja ta zwraca wskaźnik do bloku pamięci o pożądanej wielkości (lub NULL gdy zabrakło pamięci). Uwaga - może to być inny wskaźnik. Jeśli zażądamy zwiększenia rozmiaru a za zaalokowanym aktualnie obszarem nie będzie wystarczająco dużo wolnego miejsca, funkcja znajdzie nowe miejsce i przekopiuje tam starą zawartość. Jak widać, wywołanie tej funkcji może być więc kosztowne pod względem czasu.

Ostatnią funkcją jest funkcja calloc(). Przyjmuje ona dwa argumenty: liczbę elementów tablicy oraz wielkość pojedynczego elementu. Podstawową różnicą pomiędzy funkcjami malloc() i calloc() jest to, że ta druga zeruje wartość przydzielonej pamięci (do wszystkich bajtów wpisuje wartość 0).

Przykład tworzenia tablicy typu float przy użyciu calloc() zamiast malloc():

 int rozmiar;
 float *tablica;
 
 rozmiar = 3;
 tablica = (float*) calloc(rozmiar, sizeof (*tablica));
 tablica[0] = 0.1;


Inicjalizacja dynamicznej tablicy[68]

  memset (data, 0.0f , sizeof (float ) * rozmiar);


dynamicze tablice wielowymiarowe VLA

[edytuj]

Od C99, C ma tablice 2D z dynamicznymi wymiarami, czyli mogą teraz być wyrażeniem (ang. a run-time expression ) . Takie tablice nazywane są tablicami o zmiennej długości (ang. Variable Length Arrays, VLA) mogą uprościć zarządzanie pamięcią masową w programie i umożliwić użycie normalnej notacji tablicowej, nawet jeśli problem do rozwiązania wymaga, aby tablice miały różne rozmiary w różnym czasie.[69][70][71][72]

Oba wymiary takie same: [73]

double (*A)[n] = malloc(sizeof(double[n][n]));
free(a);

różne wymiary

double (*a)[y] = malloc(sizeof(double[x][y])); // 2D
double (*a)[y][z] = malloc(sizeof(double[x][y][z])); // 3D


/*
 https://stackoverflow.com/questions/365782/how-do-i-best-handle-dynamic-multi-dimensional-arrays-in-c-c


gcc a.c -Wall -Wextra
./a.out


*/

#include <stdio.h> // printf
#include <stdlib.h> // malloc



void manipulate(int rows, int cols, int (*data)[cols]) {
    for(int i=0; i < rows; i++) {
        for(int j=0; j < cols; j++) {
            printf("%d ", data[i][j]);       
        }
        printf("\n");
    }
}

int main(void) {
    int rows = 10;
    int cols = 9;
    int (*data)[cols] = malloc(rows*sizeof(*data));
    
    manipulate(rows, cols, data);
    
    free(data);
}

Możliwe deklaracje wskaźników

[edytuj]

Tutaj znajduje się krótkie kompendium jak definiować wskaźniki oraz co oznaczają poszczególne definicje:

int i;         /* zmienna całkowita (typu int) 'i' */
int *p;        /* wskaźnik 'p' wskazujący na zmienną całkowitą */
int a[];       /* tablica 'a' liczb całkowitych typu int */
int f();       /* funkcja 'f' zwracająca liczbę całkowitą typu int */
int **pp;      /* wskaźnik 'pp' wskazujący na wskaźnik wskazujący na liczbę całkowitą typu int */
int (*pa)[];   /* wskaźnik 'pa' wskazujący na tablicę liczb całkowitych typu int */
int (*pf)();   /* wskaźnik 'pf' wskazujący na funkcję zwracającą liczbę całkowitą typu int */
int *ap[];     /* tablica 'ap' wskaźników na liczby całkowite typu int */
int *fp();     /* funkcja 'fp', która zwraca wskaźnik na zmienną typu int */
int ***ppp;    /* wskaźnik 'ppp' wskazujący na wskaźnik wskazujący na wskaźnik wskazujący na liczbę  typu int */
int (**ppa)[]; /* wskaźnik 'ppa' wskazujący na wskaźnik wskazujący na tablicę liczb całkowitych typu int */
int (**ppf)(); /* wskaźnik 'ppf' wskazujący na wskaźnik funkcji zwracającej dane typu int */
int *(*pap)[]; /* wskaźnik 'pap' wskazujący na tablicę wskaźników na typ int */
int *(*pfp)(); /* wskaźnik 'pfp' na funkcję zwracającą wskaźnik na typ int*/
int **app[];   /* tablica wskaźników 'app' wskazujących na wskaźniki wskazujące na typ int */
int (*apa[])[];/* tablica wskaźników 'apa' wskazujących na tablicę liczb całkowitych typu int */
int (*apf[])();/* tablica wskaźników 'apf' na funkcje, które zwracają typ int */
int **fpp();   /* funkcja 'fpp', która zwraca wskaźnik na wskaźnik, który wskazuje typ int */
int (*fpa())[];/* funkcja 'fpa', która zwraca wskaźnik na tablicę liczb typu int */
int (*fpf())();/* funkcja 'fpf', która zwraca wskaźnik na funkcję, która zwraca dane typu int */

Popularne błędy

[edytuj]

Jednym z najczęstszych błędów, oprócz prób wykonania operacji na wskaźniku NULL, są odwołania się do obszaru pamięci po jego zwolnieniu. Po wykonaniu funkcji free() nie możemy już wykonywać żadnych odwołań do zwolnionego obszaru. Innymi rodzajami błędów są:

  1. odwołania do adresów pamięci, które są poza obszarem przydzielonym funkcją malloc() i stosem;
  2. brak sprawdzania, czy dany wskaźnik nie ma wartości NULL;
  3. wycieki pamięci, czyli gubienie wskaźników do zaalokowanej pamięci i w konsekwencji niezwalnianie całej, przydzielonej wcześniej pamięci[74];
  4. odwołania do obszarów w których nie ma prawidłowych danych (np. poprzez rzutowanie wskaźnika na nieodpowiedni typ).

Wycieki pamięci

[edytuj]

Wyciek pamięci ( ang. memory leak)

Przykład funkcji powodującej wyciek pamięci (tworzy wskaźnik, przydziela pamięć i nie zwalnia pamięci po zakończeniu funkcji): [75]

/* 
 Function with memory leak 
 http://www.geeksforgeeks.org/what-is-memory-leak-how-can-we-avoid/
*/
#include <stdlib.h>
 
int main()
{
   int *ptr = (int *) malloc(sizeof(int));
 
   /* Do some work */
 
   return 0 ; /* Return without freeing ptr*/
}

Sprawdzamy za pomocą Valgrinda

 valgrind --leak-check=full ./a.out
==3382== Memcheck, a memory error detector
==3382== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==3382== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==3382== Command: ./a.out
==3382== 
==3382== 
==3382== HEAP SUMMARY:
==3382==     in use at exit: 4 bytes in 1 blocks
==3382==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated
==3382== 
==3382== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==3382==    at 0x4C2A2DB: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3382==    by 0x40053E: main (l.c:13)
==3382== 
==3382== LEAK SUMMARY:
==3382==    definitely lost: 4 bytes in 1 blocks
==3382==    indirectly lost: 0 bytes in 0 blocks
==3382==      possibly lost: 0 bytes in 0 blocks
==3382==    still reachable: 0 bytes in 0 blocks
==3382==         suppressed: 0 bytes in 0 blocks
==3382== 
==3382== For counts of detected and suppressed errors, rerun with: -v
==3382== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 2 from 2)


Powinno być:

/* 
Function without memory leak
http://www.geeksforgeeks.org/what-is-memory-leak-how-can-we-avoid/
 */
#include <stdlib.h>;
 
int main()
{
   int *ptr = (int *) malloc(sizeof(int));
 
   /* Do some work */
 
   free(ptr);
   return 0;
}



valgrind --leak-check=full ./a.out
==3397== Memcheck, a memory error detector
==3397== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==3397== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==3397== Command: ./a.out
==3397== 
==3397== 
==3397== HEAP SUMMARY:
==3397==     in use at exit: 0 bytes in 0 blocks
==3397==   total heap usage: 1 allocs, 1 frees, 4 bytes allocated
==3397== 
==3397== All heap blocks were freed -- no leaks are possible
==3397== 
==3397== For counts of detected and suppressed errors, rerun with: -v
==3397== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)


Inne przykłady:[76].

Do znajdowania wycieków pamięci możemy użyć programów:

Zobacz też

[edytuj]

Przypisy

  1. Aby nie urażać matematyków sprecyzujmy, że chodzi o relację między zbiorami X i Y (X jest dziedziną, Y jest przeciwdziedziną) takie, że każdy element zbioru X jest w relacji z dokładnie jednym elementem ze zbioru Y.
  2. Bardziej precyzyjnie można powiedzieć, że funkcja może zwrócić tylko jedną wartość typu prostego lub jeden adres do jakiegoś obiektu w pamięci.
  3. how-to-better-name-your-functions-and-variables by Friskovec Miha
  4. Nazwy zmiennych, notacje i konwencje nazewnicze - Mateusz Skalski
  5. Kurs języka C. Autor artykułu: mgr Jerzy Wałaszek
  6. How I program C by Eskil Steenberg
  7. geeksforgeeks: c-function-argument-return-values
  8. Variadic functionsin cppreference
  9. gnu libc manual: Variadic-Functions
  10. variadic w cppreference
  11. Przykład z rosettacode
  12. stackoverflow question : passing-struct-to-function
  13. Pass array to function at java2s
  14. stackoverflow question: correct-way-of-passing-2-dimensional-array-into-a-function
  15. comp.lang.c FAQ list · Question 6.18
  16. 2 wymiarowa tablica jako argument funkcji - java2s.com
  17. how-write-good-c-main-function by By Erik O'Shaughnessy May 27, 2019
  18. opensource article: how-write-good-c-main-function
  19. Czasami można się spotkać z prototypem int main(int argc, char **argv, char **env);, który jest definiowany w standardzie POSIX, ale wykracza już poza standard C.
  20. Command-line Arguments: main( int argc, char *argv[] ) by Douglas Wilhelm Harder from University of Waterloo Canada
  21. Inne standardy mogą wymuszać istnienie tego elementu, jednak jeśli chodzi o standard języka C to nic nie stoi na przeszkodzie, aby argument argc miał wartość zero.
  22. Stackoverflow : Is “argv[0 = name-of-executable” an accepted standard or just a common convention? ]
  23. stackoverflow question: what-does-voidvar-actually-do
  24. Jeżeli ktoś lubi ekstrawagancki kod ciało funkcji main można zapisać jako return *argv ? puts(*argv), main(argc-1, argv+1) : EXIT_SUCCESS;, ale nie radzimy stosować tak skomplikowanych i, bądź co bądź, mało czytelnych konstrukcji.
  25. quora: If-we-use-void-main-why-does-the-function-give-same-output-like-int-main
  26. Uwaga! Makra EXIT_SUCCESS i EXIT_FAILURE te służą tylko i wyłącznie jako wartości do zwracania przez funkcję main(). Nigdzie indziej nie mają one zastosowania.
  27. opensource article: how-write-good-c-main-function
  28. Kompilator GCC do wersji 4.3 nie obsługuje tychże rozszerzeń
  29. stackoverflow questions: replacements-for-the-c-preprocessor
  30. Błąd rozszerzenia cite: Błąd w składni znacznika <ref>; brak tekstu w przypisie o nazwie przypis1
  31. codeforwin: c-program-to-define-undefine-redefine-macro by Pankaj Prakash
  32. mike ash : c-macro-tips-and-tricks
  33. How to properly use macros in C by Preslav Mihaylov
  34. Błąd rozszerzenia cite: Błąd w składni znacznika <ref>; brak tekstu w przypisie o nazwie przypis2
  35. brainbell: Macros_and_Miscellaneous_Pitfalls
  36. gcc online docs: Macro-Pitfalls
  37. stackoverflow question: when-and-why-use-a-define-macrox-instead-of-function
  38. wikipedia: Biblioteka_standardowa_języka_C
  39. Błąd rozszerzenia cite: Błąd w składni znacznika <ref>; brak tekstu w przypisie o nazwie printf
  40. Biblioteka_standardowa_języka_C w wikipedii
  41. wikipedia: GNU_C_Library
  42. unix.stackexchange question: what-c-library-version-does-my-system-use
  43. Błąd rozszerzenia cite: Błąd w składni znacznika <ref>; brak tekstu w przypisie o nazwie low-level
  44. devdocs C
  45. stackoverflow question : difference-between-stdint-h-and-inttypes-h
  46. GLib (G Library) w wikipedii
  47. Jakub Narębski :Zapis wyników do pliku w C, C++ i Javie
  48. gnu libc manua: Low-Level Input/Output
  49. stackoverflow question: how-to-create-custom-filenames-in-c ?
  50. gnu software: libc manual -Standard-Streams
  51. stackoverflow question: what-is-the-reason-for-fopens-failure-to-open-a-file
  52. Przykład programu wykorzystującego potok
  53. Ultimate Anti-Buddhabrot by Claude Heiland-Allen
  54. is a collection of C routines for creating and reading Portable Bit Map files (PBM).
  55. Tworzenie pliku SVG - Claudio Rocchini
  56. Variable-length_array w ang. wikipedii
  57. stackoverflow question: whats-the-point-of-vla-anyway
  58. Flexible_array_member w ang. wikipedii
  59. geeksforgeeks: dynamically-allocate-2d-array-c
  60. Row-major_order w ang wikipedii
  61. Metody obliczeniowe w nauce i technice - laboratorium informatyka II rok, Katarzyna Zając
  62. stackoverflow question : how-do-i-determine-the-size-of-my-array-in-c
  63. stackoverflow question: how-do-i-determine-the-size-of-my-array-in-c
  64. W zasadzie kompilatory mają możliwość dodania takiego sprawdzania, ale nie robi się tego, gdyż znacznie spowolniłoby to działanie programu. Takie postępowanie jest jednak pożądane w okresie testowania programu.
  65. Everything you need to know about pointers in C Version 1.3. Copyright 2005–2010 Peter Hosey.
  66. The Adventures of Malloc and New by Eunsuk Kang and Jean Y ang
  67. stackoverflow question: i-dont-understand-how-this-code-replaces-the-head-of-a-linked-list-or-how-to ?
  68. stackoverflow question: initializing-c-dynamic-arrays
  69. The New C:Why Variable Length Arrays? By Randy Meyers, October 01, 2001
  70. The New C: By Randy Meyers, December 01, 2001
  71. The New C:By Randy Meyers, January 01, 2002
  72. The New C: By Randy Meyers, March 01, 2002
  73. stackoverflow question: how-do-i-work-with-dynamic-multi-dimensional-arrays-in-c
  74. Wyciek pamięci w polskiej wikipedii
  75. What is Memory Leak? How can we avoid? February 6, 2010
  76. Create a memory leak, without any fork bombs


Napisy

[edytuj]

W dzisiejszych czasach komputer przestał być narzędziem tylko i wyłącznie do przetwarzania danych. Od programów komputerowych zaczęto wymagać czegoś nowego - program w wyniku swojego działania nie ma zwracać danych, rozumianych tylko przez autora programu, lecz powinien być na tyle komunikatywny, aby przeciętny użytkownik komputera mógł bez problemu tenże komputer obsłużyć. Do przechowywania tychże komunikatów służą tzw. "łańcuchy" (ang. string) czyli ciągi znaków.

Język C nie jest wygodnym narzędziem do manipulacji napisami. Jak się wkrótce przekonamy, zestaw funkcji umożliwiających operacje na napisach w bibliotece standardowej C jest raczej skromny. Dodatkowo, problemem jest sposób, w jaki łańcuchy przechowywane są w pamięci. Zobaczymy także, jak stworzyć łańcuch typu lista połączona

Łańcuchy znaków w języku C

[edytuj]

Napis jest zapisywany w kodzie programu jako ciąg znaków zawarty pomiędzy dwoma cudzysłowami.

 printf ("Napis w języku C");

W pamięci taki łańcuch jest następującym po sobie ciągiem znaków (char), który kończy się znakiem "null" zapisywanym jako '\0'.

Jeśli mamy napis, do poszczególnych znaków odwołujemy się jak w tablicy:

 const char *tekst = "Jakiś tam tekst";
 printf("%c\n", "przykład"[0]); /* wypisze p - znaki w napisach są numerowane od zera */
 printf("%c\n", tekst[2]);      /* wypisze k */

Ponieważ napis w pamięci kończy się zerem umieszczonym tuż za jego zawartością, odwołanie się do znaku o indeksie równym długości napisu zwróci zero:

 printf("%d", "test"[4]);       /* wypisze 0 */

Napisy możemy wczytywać z klawiatury i wypisywać na ekran przy pomocy dobrze znanych funkcji scanf, printf i pokrewnych. Formatem używanym dla napisów jest %s.

 printf("%s", tekst);

Większość funkcji działających na napisach znajduje się w pliku nagłówkowym string.h.


Jeśli łańcuch jest zbyt długi, można zapisać go w kilku linijkach, ale wtedy przechodząc do następnej linii musimy na końcu postawić znak "\".

 printf("Ten napis zajmuje \
 więcej niż jedną linię");

Instrukcja taka wydrukuje:

Ten napis zajmuje więcej niż jedną linię

Możemy zauważyć, że napis, który w programie zajął więcej niż jedną linię, na ekranie zajął tylko jedną. Jest tak, ponieważ "\" informuje kompilator, że łańcuch będzie kontynuowany w następnej linii kodu - nie ma wpływu na prezentację łańcucha. Aby wydrukować napis w kilku liniach należy wstawić do niego \n ("n" pochodzi tu od "new line", czyli "nowa linia").

 printf("Ten napis\nna ekranie\nzajmie więcej niż jedną linię.");

W wyniku otrzymamy:

Ten napis
na ekranie
zajmie więcej niż jedną linię.

Jak komputer przechowuje w pamięci łańcuch?

[edytuj]
Napis "Merkkijono" przechowywany w pamięci

Zmienna, która przechowuje łańcuch znaków, jest tak naprawdę wskaźnikiem do ciągu znaków (bajtów) w pamięci. Możemy też myśleć o napisie jako o tablicy znaków (jak wyjaśnialiśmy wcześniej, tablice to też wskaźniki).


Możemy wygodnie zadeklarować napis:

 const char *tekst  = "Jakiś tam tekst"; /* Umieszcza napis w obszarze danych programu i przypisuje adres */
 char tekst[] = "Jakiś tam tekst"; /* Umieszcza napis w tablicy */
 char tekst[] = {'J','a','k','i','s',' ','t','a','m',' ','t','e','k','s','t','\0'};
                /* Tekst to taka tablica jak każda inna */

Kompilator automatycznie przydziela wtedy odpowiednią ilość pamięci (tyle bajtów, ile jest liter plus jeden dla kończącego nulla). Jeśli natomiast wiemy, że dany łańcuch powinien przechowywać określoną ilość znaków (nawet, jeśli w deklaracji tego łańcucha podajemy mniej znaków) deklarujemy go w taki sam sposób, jak tablicę jednowymiarową:

 char tekst[80] = "Ten tekst musi być krótszy niż 80 znaków";

Należy cały czas pamiętać, że napis jest tak naprawdę tablicą. Jeśli zarezerwowaliśmy dla napisu 80 znaków, to przypisanie do niego dłuższego napisu spowoduje pisanie po pamięci.

Pisanie po pamięci może czasami skończyć się błędem dostępu do pamięci ("segmentation fault" w systemach UNIX) i zamknięciem programu, jednak może zdarzyć się jeszcze gorsza ewentualność - możemy zmienić w ten sposób przypadkowo wartość innych zmiennych. Program zacznie wtedy zachowywać się nieprzewidywalnie - zmienne a nawet stałe, co do których zakładaliśmy, że ich wartość będzie ściśle ustalona, mogą przyjąć taką wartość, jaka absolutnie nie powinna mieć miejsca.

Kluczowy jest też kończący napis znak null. W zasadzie wszystkie funkcje operujące na napisach opierają właśnie na nim. Na przykład, strlen szuka rozmiaru napisu idąc od początku i zliczając znaki, aż nie natrafi na znak o kodzie zero. Jeśli nasz napis nie kończy się znakiem null, funkcja będzie szła dalej po pamięci. Na szczęście, wszystkie operacje podstawienia typu tekst = "Tekst" powodują zakończenie napisu nullem (o ile jest na niego miejsce) [2].

Znaki specjalne

[edytuj]

Jak zapewne zauważyłeś w poprzednim przykładzie, w łańcuchu ostatnim znakiem jest znak o wartości zero ('\0')[3]. Jednak łańcuchy mogą zawierać inne znaki specjalne(sekwencje sterujące)[4], np.:

  • '\a' - alarm (sygnał akustyczny terminala)
  • '\b' - backspace (usuwa poprzedzający znak)
  • '\f' - wysuniecie strony (np. w drukarce)
  • '\r' - powrót kursora (karetki) do początku wiersza
  • '\n' - znak nowego wiersza
  • '\"' - cudzysłów
  • '\'' - apostrof
  • '\\' - ukośnik wsteczny (backslash)
  • '\t' - tabulacja pozioma
  • '\v' - tabulacja pionowa
  • '\?' - znak zapytania (pytajnik)
  • '\ooo' - liczba zapisana w systemie oktalnym (ósemkowym), gdzie 'ooo' należy zastąpić trzycyfrową liczbą w tym systemie
  • '\xhh' - liczba zapisana w systemie heksadecymalnym (szesnastkowym), gdzie 'hh' należy zastąpić dwucyfrową liczbą w tym systemie
  • '\unnnn' - uniwersalna nazwa znaku, gdzie 'nnnn' należy zastąpić czterocyfrowym identyfikatorem znaku w systemie szesnatkowym. 'nnnn' odpowiada dłuższej formie w postaci '0000nnnn'
  • '\unnnnnnnn' - uniwersalna nazwa znaku, gdzie 'nnnnnnnn' należy zastąpić ośmiocyfrowym identyfikatorem znaku w systemie szesnatkowym.

Warto zaznaczyć, że znak nowej linii ('\n') jest w różny sposób przechowywany w różnych systemach operacyjnych. Wiąże się to z pewnymi historycznymi uwarunkowaniami. W niektórych systemach używa się do tego jednego znaku o kodzie 0x0A (Line Feed - nowa linia). Do tej rodziny zaliczamy systemy z rodziny Unix: Linux, *BSD, Mac OS X inne. Drugą konwencją jest zapisywanie '\n' za pomocą dwóch znaków: LF (Line Feed) + CR (Carriage return - powrót karetki). Znak CR reprezentowany jest przez wartość 0x0D. Kombinacji tych dwóch znaków używają m.in.: CP/M, DOS, OS/2, Microsoft Windows. Trzecia grupa systemów używa do tego celu samego znaku CR. Są to systemy działające na komputerach Commodore, Apple II oraz Mac OS do wersji 9. W związku z tym plik utworzony w systemie Linux może prezentować się inaczej pod systemem Windows.

Operacje na łańcuchach

[edytuj]

nagłówki

[edytuj]

Większość funkcji operujących na ciągach C jest zadeklarowana w nagłówku string.h (cstring w C++), natomiast funkcje operujące na ciągach C są deklarowane w nagłówku wchar.h

deklaracja

[edytuj]
// http://www.crasseux.com/books/ctutorial/Initializing-strings.html

#include <stdio.h>
#include <string.h>

int main()
{

  /* Example 1 */
  char string1[] = "A string declared as an array.\n";

  /* Example 2 */
  const char *string2 = "A string declared as a pointer.\n";

  /* Example 3 */
  char string3[30];
  strcpy(string3, "A string constant copied in.\n");

  printf ("%s\n", string1);
  printf ("%s\n",string2);
  printf ("%s\n",string3);

  return 0;
}


Porównywanie łańcuchów

[edytuj]

Napisy to tak naprawdę wskaźniki. Tak więc używając zwykłego operatora porównania ==, otrzymamy wynik porównania adresów a nie tekstów.

Do porównywania dwóch ciągów znaków należy użyć funkcji strcmp zadeklarowanej w pliku nagłówkowym string.h. Jako argument przyjmuje ona dwa napisy i zwraca wartość ujemną jeżeli napis pierwszy jest mniejszy od drugiego, 0 jeżeli napisy są równe lub wartość dodatnią jeżeli napis pierwszy jest większy od drugiego. Ciągi znaków porównywalne są leksykalnie kody znaków, czyli np. (przyjmując kodowanie ASCII) "a" jest mniejsze od "b", ale jest większe od "B". Np.:

 #include <stdio.h>
 #include <string.h>
 
 int main(void) {
   char str1[100] = {'\0'}, str2[100] = {'\0'};
   int cmp;
 
   puts("Podaj dwa ciagi znakow: ");
   fgets(str1, sizeof str1, stdin);
   fgets(str2, sizeof str2, stdin);
 
   cmp = strcmp(str1, str2);
   if (cmp<0) {
     puts("Pierwszy napis jest mniejszy.");
   } else if (cmp>0) {
     puts("Pierwszy napis jest wiekszy.");
   } else {
     puts("Napisy sa takie same.");
   }
 
   return 0;
 }

Czasami możemy chcieć porównać tylko fragment napisu, np. sprawdzić czy zaczyna się od jakiegoś ciągu. W takich sytuacjach pomocna jest funkcja strncmp. W porównaniu do strcmp() przyjmuje ona jeszcze jeden argument oznaczający maksymalną liczbę znaków do porównania:

 #include <stdio.h>
 #include <string.h>
 
 int main(void) {
   char str[100];
   int cmp;
 
   fputs("Podaj ciag znakow: ", stdout);
   fgets(str, sizeof str, stdin);
 
   if (!strncmp(str, "foo", 3)) {
     puts("Podany ciag zaczyna sie od 'foo'.");
   }
 
   return 0;
 }

Kopiowanie napisów

[edytuj]

Do kopiowania ciągów znaków służy funkcja strcpy, która kopiuje drugi napis w miejsce pierwszego. Musimy pamiętać, by w pierwszym łańcuchu było wystarczająco dużo miejsca.

 char napis[100];
 strcpy(napis, "Ala ma kota.");

Znacznie bezpieczniej jest używać funkcji strncpy, która kopiuje co najwyżej tyle bajtów ile podano jako trzeci parametr. Uwaga! Jeżeli drugi napis jest za długi funkcja nie kopiuje znaku null na koniec pierwszego napisu, dlatego zawsze trzeba to robić ręcznie:

 char napis[100] = { 0 };
 strncpy(napis, "Ala ma kota.", sizeof(napis) - 1);

Łączenie napisów

[edytuj]

Do łączenia napisów służy funkcja strcat, która kopiuje drugi napis do pierwszego. Ponownie jak w przypadku strcpy musimy zagwarantować, by w pierwszym łańcuchu było wystarczająco dużo miejsca.

 #include <stdio.h>
 #include <string.h>
 
 int main(void) {
   char napis1[80] = "hello ";
   const const char *napis2 = "world";
   strcat(napis1, napis2);
   puts(napis1);
   return 0;
 }

I ponownie jak w przypadku strcpy istnieje funkcja strncat, która skopiuje co najwyżej tyle bajtów ile podano jako trzeci argument i dodatkowo dopisze znak null.

 #include <stdio.h>
 #include <string.h>
 
 int main(void) {
   char napis1[80] = "hello ";
   const char *napis2 = "world";
   strncat(napis1, napis2, 2);
   puts(napis1);
   return 0;
 }
hello wo

Możemy też wykorzystać trzeci argument do zapewnienia bezpiecznego wywołania funkcji kopiującej. W przypadku zbyt małej tablicy skopiowany zostanie fragment tylko takie długości, na jaki starczy miejsca (uwzględniając, że na końcu trzeba dodać znak '\0'). Przy podawaniu ilości znaków należy także pamiętać, że łańcuch, do którego kopiujemy nie musi być pusty, a więc część pamięci przeznaczona na niego jest już zajęta, jak w poniższym przykładzie. Dlatego od rozmiaru całego łańcucha do którego kopiujemy należy odjąć długość napisu, który już się w nim znajduje.

   char napis1[10] = "hello ";
   const char *napis2 = "world";
   strncat(napis1, napis2, sizeof(napis1)-strlen(napis1)- 1);
   puts(napis1);
hello wor


Bezpieczeństwo kodu a łańcuchy

[edytuj]

Przepełnienie bufora

[edytuj]

O co właściwie chodzi z tymi funkcjami strncpy i strncat? Otóż, niewinnie wyglądające łańcuchy mogą okazać się zabójcze dla bezpieczeństwa programu, a przez to nawet dla systemu, w którym ten program działa. Może brzmi to strasznie, lecz jest to prawda. Może pojawić się tutaj pytanie: "w jaki sposób łańcuch może zaszkodzić programowi?". Otóż może i to całkiem łatwo. Przeanalizujmy następujący kod:

 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 int main(int argc , const char **argv) {
   char haslo_poprawne = 0;
   char haslo[16];
   
   if (argc!=2) {
     fprintf(stderr, "uzycie: %s haslo", argv[0]);
     return EXIT_FAILURE;
   }
   
   strcpy(haslo, argv[1]); /* tutaj następuje przepełnienie bufora */
   if (!strcmp(haslo, "poprawne")) {
     haslo_poprawne = 1;
   }
   
   if (!haslo_poprawne) {
     fputs("Podales bledne haslo.\n", stderr);
     return EXIT_FAILURE;
   }
   
   puts("Witaj, wprowadziles poprawne haslo.");
   return EXIT_SUCCESS;
 }

Jest to bardzo prosty program, który wykonuje jakąś akcję, jeżeli podane jako pierwszy argument hasło jest poprawne. Sprawdźmy czy działa:

$ ./a.out niepoprawne
Podales bledne haslo.
$ ./a.out poprawne
Witaj, wprowadziles poprawne haslo.

Jednak okazuje się, że z powodu użycia funkcji strcpy włamywacz nie musi znać hasła, aby program uznał, że zna hasło, np.:

$ ./a.out 11111111111111111111111111111111
Witaj, wprowadziles poprawne haslo.

Co się stało? Podaliśmy ciąg jedynek dłuższy niż miejsce przewidziane na hasło. Funkcja strcpy() kopiując znaki z argv[1] do tablicy (bufora) haslo przekroczyła przewidziane dla niego miejsce i szła dalej - gdzie znajdowała się zmienna haslo_poprawne. strcpy() kopiowała znaki już tam, gdzie znajdowały się inne dane — między innymi wpisała jedynkę do haslo_poprawne.


Podany przykład może się różnie zachowywać w zależności od kompilatora, jakim został skompilowany, i systemu, na jakim działa, ale ogólnie mamy do czynienia z poważnym niebezpieczeństwem.

Oto bezpieczna wersja poprzedniego programu:

 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 
 int main(int argc, const char **argv) {
   char haslo_poprawne = 0;
   char haslo[16];
   
   if (argc!=2) {
     fprintf(stderr, "uzycie: %s haslo", argv[0]);
     return EXIT_FAILURE;
   }
   
   strncpy(haslo, argv[1], sizeof(haslo) - 1);
   haslo[sizeof haslo - 1] = '\0';
   if (!strcmp(haslo, "poprawne")) {
     haslo_poprawne = 1;
   }
   
   if (!haslo_poprawne) {
     fputs("Podales bledne haslo.\n", stderr);
     return EXIT_FAILURE;
   }
   
   puts("Witaj, wprowadziles poprawne haslo.");
   return EXIT_SUCCESS;
 }

Bezpiecznymi alternatywami do strcpy i strcat są też funkcje strlcpy oraz strlcat opracowane przez projekt OpenBSD i dostępne do ściągnięcia na wolnej licencji: strlcpy, strlcat. strlcpy() działa podobnie do strncpy: strlcpy (buf, argv[1], sizeof buf);, jednak jest szybsza (nie wypełnia pustego miejsca zerami) i zawsze kończy napis nullem (czego nie gwarantuje strncpy). strlcat(dst, src, size) działa natomiast jak strncat(dst, src, size-1).

Do innych niebezpiecznych funkcji należy np. gets zamiast której należy używać fgets.

Zawsze możemy też alokować napisy dynamicznie:

 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 
 int main(int argc, const char **argv) {
   char haslo_poprawne = 0;
   const char *haslo;
   
   if (argc!=2) {
     fprintf(stderr, "uzycie: %s haslo", argv[0]);
     return EXIT_FAILURE;
   }
   
   haslo = malloc(strlen(argv[1]) + 1); /* +1 dla znaku null */
   if (!haslo) {
     fputs("Za malo pamieci.\n", stderr);
     return EXIT_FAILURE;
   }
   
   strcpy(haslo, argv[1]);
   if (!strcmp(haslo, "poprawne")) {
     haslo_poprawne = 1;
   }
   
   if (!haslo_poprawne) {
     fputs("Podales bledne haslo.\n", stderr);
     return EXIT_FAILURE;
   }
   puts("Witaj, wprowadziles poprawne haslo.");
   free(haslo);
   return EXIT_SUCCESS;
 }


Nadużycia z udziałem ciągów formatujących

[edytuj]

Jednak to nie koniec kłopotów z napisami. Wielu programistów, nieświadomych zagrożenia często używa tego typu konstrukcji:

 #include <stdio.h>
 int main (int argc, const char **argv)
 {
   printf (argv[1]);
   return 0;
 }

Z punktu widzenia bezpieczeństwa jest to bardzo poważny błąd programu, który może nieść ze sobą katastrofalne skutki! Prawidłowo napisany kod powinien wyglądać następująco:

 #include <stdio.h>
 int main (int argc, const char **argv)
 {
   printf ("%s", argv[1]);
   return 0;
 }

lub:

 #include <stdio.h>
 int main (int argc, const char **argv)
 {
   fputs (argv[1], stdout);
   return 0;
 }

Źródło problemu leży w konstrukcji funkcji printf. Przyjmuje ona bowiem za pierwszy parametr łańcuch, który następnie przetwarza. Jeśli w pierwszym parametrze wstawimy jakąś zmienną, to funkcja printf potraktuje ją jako ciąg znaków razem ze znakami formatującymi. Zatem ważne, aby wcześnie wyrobić sobie nawyk stosowania funkcji printf z co najmniej dwoma parametrami, nawet w przypadku wyświetlenia samego tekstu.

Konwersje

[edytuj]

Czasami zdarza się, że łańcuch można interpretować nie tylko jako ciąg znaków, lecz np. jako liczbę. Jednak, aby dało się taką liczbę przetworzyć musimy skopiować ją do pewnej zmiennej. Aby ułatwić programistom tego typu zamiany powstał zestaw funkcji bibliotecznych. Należą do nich:

  • atol, strtol - zamienia łańcuch na liczbę całkowitą typu long
  • atoi - zamienia łańcuch na liczbę całkowitą typu int
  • atoll, strtoll - zamienia łańcuch na liczbę całkowitą typu long long (64 bity); dodatkowo istnieje przestarzała funkcja atoq będąca rozszerzeniem GNU,
  • atof, strtod - przekształca łańcuch na liczbę typu double

Ogólnie rzecz ujmując funkcje z serii ato* nie pozwalają na wykrycie błędów przy konwersji i dlatego, gdy jest to potrzebne, należy stosować funkcje strto*.

Czasami przydaje się też konwersja w drugą stronę, tzn. z liczby na łańcuch. Do tego celu może posłużyć funkcja sprintf lub snprintf. sprintf jest bardzo podobna do printf, tyle, że wyniki jej prac zwracane są do pewnego łańcucha, a nie wyświetlane np. na ekranie monitora. Należy jednak uważać przy jej użyciu (patrz - Bezpieczeństwo kodu a łańcuchy). snprintf (zdefiniowana w nowszym standardzie) dodatkowo przyjmuje jako argument wielkość bufora docelowego.

Operacje na znakach

[edytuj]

Warto też powiedzieć w tym miejscu o operacjach na samych znakach. Spójrzmy na poniższy program:

 #include <stdio.h>
 #include <ctype.h>
 #include <string.h>
 
 int main()
 {
   int znak;
   while ((znak = getchar())!=EOF) {
     if( islower(znak) ) {
       znak = toupper(znak);
     } else if( isupper(znak) ) {
       znak = tolower(znak);
     }
     putchar(znak);
   }
   return 0;
 }

Program ten zmienia we wczytywanym tekście wielkie litery na małe i odwrotnie. Wykorzystujemy funkcje operujące na znakach z pliku nagłówkowego ctype.h. isupper sprawdza, czy znak jest wielką literą, natomiast toupper zmienia znak (o ile jest literą) na wielką literę. Analogicznie jest dla funkcji islower i tolower.

Jako ćwiczenie, możesz tak zmodyfikować program, żeby odczytywał dane z pliku podanego jako argument lub wprowadzonego z klawiatury.

Częste błędy

[edytuj]
  • pisanie do niezaalokowanego miejsca
 const char *tekst;
 scanf("%s", tekst);
  • zapominanie o kończącym napis nullu
 char test[4] = "test"; /* nie zmieścił się null kończący napis */
  • nieprawidłowe porównywanie łańcuchów
 char tekst1[] = "jakis tekst";
 char tekst2[] = "jakis tekst";
 if( tekst1 == tekst2 ) { /* tu zawsze będzie fałsz bo == porównuje adresy, należy użyć strcmp().  */
    ...
 }

Takich wad nie ma łańcuch typu linked-list.

Unicode

[edytuj]

W dzisiejszych czasach brak obsługi wielu języków praktycznie marginalizowałoby język. Dlatego też C99 wprowadza możliwość zapisu znaków wg norm Unicode.

Jaki typ?

[edytuj]

Do przechowywania znaków zakodowanych w Unicode powinno się korzystać z typu wchar_t. Jego domyślny rozmiar jest zależny od użytego kompilatora, lecz w większości zaktualizowanych kompilatorów powinny to być 2 bajty. Typ ten jest częścią języka C++, natomiast w C znajduje się w pliku nagłówkowym stddef.h.

Alternatywą jest wykorzystanie gotowych bibliotek dla Unicode (większość jest dostępnych jedynie dla C++, nie współpracuje z C), które często mają zdefiniowane własne typy, jednak zmuszeni jesteśmy wtedy do przejścia ze znanych nam już funkcji jak np. strcpy, strcmp na funkcje dostarczane przez bibliotekę, co jest dość niewygodne. My zajmiemy się pierwszym wyjściem.


Nazwa Opis
NULL Macro expanding to the null pointer constant; that is, a constant representing a pointer value which is guaranteed not to be a valid address of an object in memory.
wchar_t Type used for a code unit in "wide" strings. On Windows, the only platform to use wchar_t extensively, it's defined as 16-bit[5] which was enough to represent any Unicode (UCS-2) character, but is now only enough to represent a UTF-16 code unit, which can be half a code point. On other platforms it is defined as 32-bit and a Unicode code point always fits. The C standard only requires that wchar_t be wide enough to hold the widest character set among the supported system locales[6] and be greater or equal in size to char,[7]
wint_t Integer type that can hold any value of a wchar_t as well as the value of the macro WEOF. This type is unchanged by integral promotions. Usually a 32 bit signed value.
char8_t[8] Part of the C standard since C23, in <uchar.h>, a type that is suitable for storing UTF-8 characters.[9]
char16_t[10] Part of the C standard since C11,[11] in <uchar.h>, a type capable of holding 16 bits even if wchar_t is another size. If the macro __STDC_UTF_16__ is defined as 1, the type is used for UTF-16 on that system. This is always the case in C23.[12] C++ does not define such a macro, but the type is always used for UTF-16 in that language.[13]
char32_t[10] Part of the C standard since C11,[14] in <uchar.h>, a type capable of holding 32 bits even if wchar_t is another size. If the macro __STDC_UTF_32__ is defined as 1, the type is used for UTF-32 on that system. This is always the case in C23. [12] C++ does not define such a macro, but the type is always used for UTF-32 in that language.[13]
mbstate_t Contains all the information about the conversion state required from one call to a function to the other.


Biblioteki

[edytuj]
  • wchar.h
  • gotowe bibliotek dla Unicode (większość jest dostępnych jedynie dla C++, nie współpracuje z C), które często mają zdefiniowane własne typy, jednak zmuszeni jesteśmy wtedy do przejścia ze znanych nam już funkcji jak np. strcpy, strcmp na funkcje dostarczane przez bibliotekę, co jest dość niewygodne.

Jaki rozmiar i jakie kodowanie?

[edytuj]

Unicode [15]określa jedynie jakiej liczbie odpowiada jaki znak, nie mówi zaś nic o sposobie dekodowania (tzn. jaka sekwencja znaków odpowiada jakiemu znaku/znakom). Jako że Unicode obejmuje 918 tys. znaków, zmienna zdolna pomieścić go w całości musi mieć przynajmniej 3 bajty. Niestety procesory nie funkcjonują na zmiennych o tym rozmiarze, pracują jedynie na zmiennych o wielkościach: 1, 2, 4 oraz 8 bajtów (kolejne potęgi liczby 2). Dlatego też jeśli wciąż uparcie chcemy być dokładni i zastosować przejrzyste kodowanie musimy skorzystać ze zmiennej 4-bajtowej (32 bity). Tak do sprawy podeszli twórcy kodowania Unicode nazwanego UTF-32/UCS-4.

Ten typ kodowania po prostu przydziela każdemu znakowi Unicode kolejne liczby. Jest to najbardziej intuicyjny i wygodny typ kodowania, ale jak widać ciągi znaków zakodowane w nim są bardzo obszerne, co zajmuje dostępną pamięć, spowalnia działanie programu oraz drastycznie pogarsza wydajność podczas transferu przez sieć. Poza UTF-32 istnieje jeszcze wiele innych kodowań. Najpopularniejsze z nich to:

  • UTF-8 - od 1 do 6 bajtów (dla znaków poniżej 65536 do 3 bajtów) na znak przez co jest skrajnie niewygodny, gdy chcemy przeprowadzać jakiekolwiek operacje na tekście bez korzystania z gotowych funkcji
  • UTF-16 - 2 lub 4 bajty na znak; ręczne modyfikacje łańcucha są bardziej skomplikowane niż przy UTF-32
  • UCS-2 - 2 bajty na znak przez co znaki z numerami powyżej 65 535 nie są uwzględnione; równie wygodny w użytkowaniu co UTF-32.

Ręczne operacje na ciągach zakodowanych w UTF-8 i UTF-16 są utrudnione, ponieważ w przeciwieństwie do UTF-32, gdzie można określić, iż powiedzmy 2. znak ciągu zajmuje bajty od 4. do 7. (gdyż z góry wiemy, że 1. znak zajął bajty od 0. do 3.), w tych kodowaniach musimy najpierw określić rozmiar 1. znaku. Ponadto, gdy korzystamy z nich nie działają wtedy funkcje udostępniane przez biblioteki C do operowania na ciągach znaków.

Priorytet Proponowane kodowania
mały rozmiar UTF-8
łatwa i wydajna edycja UTF-32 lub UCS-2
przenośność UTF-8[16]
ogólna szybkość UCS-2 lub UTF-8

Co należy zrobić, by zacząć korzystać z kodowania UCS-2 (domyślne kodowanie dla C):

  • powinniśmy korzystać z typu wchar_t (ang. "wide character"), jednak jeśli chcemy udostępniać kod źródłowy programu do kompilacji na innych platformach, powinniśmy ustawić odpowiednie parametry dla kompilatorów, by rozmiar był identyczny niezależnie od platformy.
  • korzystamy z odpowiedników funkcji operujących na typie char pracujących na wchar_t (z reguły składnia jest identyczna z tą różnicą, że w nazwach funkcji zastępujemy "str" na "wcs" np. strcpy - wcscpy; strcmp - wcscmp)
  • jeśli przyzwyczajeni jesteśmy do korzystania z klasy string (tylko C++), powinniśmy zamiast niej korzystać z wstring, która posiada zbliżoną składnię, ale pracuje na typie wchar_t.

Co należy zrobić, by zacząć korzystać z Unicode:

  • gdy korzystamy z kodowań innych niż UTF-16 i UCS-2, powinniśmy zdefiniować własny typ
  • w wykorzystywanych przez nas bibliotekach podajemy typ wykorzystanego kodowania.
  • gdy chcemy ręcznie modyfikować ciąg musimy przeczytać specyfikację danego kodowania; są one wyczerpująco opisane na siostrzanym projekcie Wikibooks - Wikipedii.

Przykład użycia kodowania UCS-2:

#include <stddef.h> /* jeśli używamy C++, możemy opuścić tę linijkę */
#include <stdio.h>
#include <string.h>

int main() {
  wchar_t* wcs1 = L"Ala ma kota.";
  wchar_t* wcs2 = L"Kot ma Ale.";
  wchar_t calosc[25];
  
  wcscpy(calosc, wcs1);
  *(calosc + wcslen(wcs1)) = L' ';
  wcscpy(calosc + wcslen(wcs1) + 1, wcs2);
  
  printf("lancuch wyjsciowy: %ls\n", calosc);
  return 0;
}


Typy złożone

[edytuj]

typedef

[edytuj]

Jest to słowo kluczowe, które służy do definiowania typów pochodnych np.:

 typedef stara_nazwa  nowa_nazwa;
 typedef int mojInt;
 typedef int* WskNaInt;

od tej pory można używać typów mojInt i WskNaInt.

Często używa się typedef w jednej instrukcji razem z definicją typu

Typ wyliczeniowy

[edytuj]

Służy do tworzenia zmiennych, które mogą przyjmować tylko pewne z góry ustalone wartości:

 enum Nazwa {WARTOSC_1, WARTOSC_2, WARTOSC_N };

Na przykład można w ten sposób stworzyć zmienną przechowującą kierunek:

 enum Kierunek {W_GORE, W_DOL, W_LEWO, W_PRAWO};
   
 enum Kierunek ruch = W_GORE;

Gdzie "Kierunek" to typ zmiennej, wcześniej określony, a "ruch" nazwa zmiennej, o takim typie. Zmienną tę można teraz wykorzystać na przykład w instrukcji switch

 switch(ruch)
 {
   case W_GORE:
     printf("w górę\n");
     break;
   case W_DOL:
     printf("w dół\n");
     break;
   default:
     printf("gdzieś w bok\n");
 }

Tradycyjnie przechowywane wielkości zapisuje się wielkimi literami (W_GORE, W_DOL).

Tak naprawdę C przechowuje wartości typu wyliczeniowego jako liczby całkowite (zakres typu signed int), o czym można się łatwo przekonać:

 ruch = W_DOL;
 printf("%i\n", ruch); /* wypisze 1 */

Kolejne wartości to po prostu liczby całkowite: domyślnie pierwsza to zero, druga jeden itp. Możemy przy deklarowaniu typu wyliczeniowego zmienić domyślne przyporządkowanie:

 enum Kierunek { W_GORE, W_DOL = 8, W_LEWO, W_PRAWO };
 printf("%i %i\n", W_DOL, W_LEWO); /* wypisze 8 9 */

Co więcej liczby mogą się powtarzać i wcale nie muszą być ustawione w kolejności rosnącej:

 enum Kierunek { W_GORE = 5, W_DOL = 5, W_LEWO = 2, W_PRAWO = -1 };
 printf("%i %i\n", W_DOL, W_LEWO); /* wypisze 5 2 */

Traktowanie przez kompilator typu wyliczeniowego jako liczby pozwala na wydajną ich obsługę, ale stwarza niebezpieczeństwa:

Można przypisywać pod typ wyliczeniowy liczby, nawet nie mające odpowiednika w wartościach, a kompilator może o tym nawet nie ostrzec:

 ruch = 40;

Lub przypisać pod typ wyliczeniowy, np. liczby:

 enum Kierunek { W_GORE, W_DOL, W_LEWO = -1, W_PRAWO };

Co spowoduje nadanie tej samej wartości 0 dla elementów W_GORE i W_PRAWO, a to może skutkować błędem kompilacji, np. w przytoczonym powyżej użyciu instrukcji switch.

Struktury

[edytuj]

Struktury to specjalny typ danych mogący przechowywać wiele wartości w jednej zmiennej. Od tablic jednakże różni się tym, iż te wartości mogą być różnych typów.

Struktury definiuje się w następujący sposób:

 struct Struktura {
   int pole1;
   int pole2;
   char pole3;
 };

gdzie "Struktura" to nazwa tworzonej struktury.
Nazewnictwo, ilość i typ pól definiuje programista według własnego uznania.

Zmienną posiadającą strukturę tworzy się podając jako jej typ nazwę struktury.

 struct Struktura zmiennaS;


Dostęp do poszczególnych pól uzyskuje się przy pomocy operatora wyboru składnika: kropki ('.').

 zmiennaS.pole1 = 60;   /* przypisanie liczb do pól */
 zmiennaS.pole2 = 2;
 zmiennaS.pole3 = 'a';  /* a teraz znaku */



typedef struct

[edytuj]

Deklaracja typu strukturalnego

Definicja nienazwanego typu strukturalnego:[17]

typedef struct {
    int x;
    int y;
} TypPunktowy;

TypPunktowy to jest

  • alias do typu
  • krótka nazwa typu ( pełna nazwa to: struct TypPunktowy)


W obrębie struktury nie można odwoływać się do siebie za pomocą krótkiej nazwy.


Struktury jako argumenty funkcji mogą być użyte na 2 sposoby:[18]

  • przekazywanie wartości bez możliwości ich zmiany ( ang. pass by value)
  • przekazywanie wartości z możliwością ich zmiany ( ang. pass by reference)


/*
https://en.cppreference.com/w/c/language/struct_initialization

gcc s.c -Wall -Wextra
./a.out

*/
#include <stdio.h>


// definicja typu
typedef struct {   
    char name[20];
    char sex; 
    int age; 
} person_t;


// zmienna
person_t p = {"Tom", 'M', 19};


int main(void)
{

	 printf("person name = %s, sex = %d age = %d \n", p.name, p.sex, p.age);


	return 0;
}

Unie

[edytuj]

Unie to kolejny sposób prezentacji danych w pamięci. Na pierwszy rzut oka wyglądają bardzo podobnie do struktur:

 union Nazwa {
   typ1 nazwa1;
   typ2 nazwa2;
   /* ... */
 };

Na przykład:

 union LiczbaLubZnak {
   int calkowita;
   char znak;
   double rzeczywista;
 };

Pola w unii nakładają się na siebie w ten sposób, że w danej chwili można w niej przechowywać wartość tylko jednego typu. Unia zajmuje w pamięci tyle miejsca, ile zajmuje największa z jej składowych. W powyższym przypadku unia będzie miała prawdopodobnie rozmiar typu double czyli często 64 bity, a całkowita i znak będą wskazywały odpowiednio na pierwsze cztery bajty lub na pierwszy bajt unii (choć nie musi tak być zawsze). Dlaczego tak? Taka forma często przydaje się np. do konwersji pomiędzy różnymi typami danych. Możemy dzięki unii podzielić zmienną 32-bitową na cztery składowe zmienne o długości 8 bitów każda.

Do konkretnych wartości pól unii odwołujemy się, podobnie jak w przypadku struktur, za pomocą kropki:

 union LiczbaLubZnak liczba;
 liczba.calkowita = 10;
 printf("%d\n", liczba.calkowita);

Zazwyczaj użycie unii ma na celu zmniejszenie zapotrzebowania na pamięć, gdy naraz będzie wykorzystywane tylko jedno pole i jest często łączone z użyciem struktur.

Przyjrzyjmy się teraz przykładowi, który powinien dobitnie zademonstrować działanie unii:

 #include <stdio.h>
 
 struct adres_bajtowy {
  __uint8_t a;
  __uint8_t b;
  __uint8_t c;
  __uint8_t d;
  };
 
 union adres {
   __uint32_t ip;
   struct adres_bajtowy badres;
   };
 
 int main ()
 {
    union adres addr;
    addr.badres.a = 192;
    addr.badres.b = 168;
    addr.badres.c = 1;
    addr.badres.d = 1;
    printf ("Adres IP w postaci 32-bitowej zmiennej: %08x\n",addr.ip);
    return 0;
 }

Zauważyłeś pewien ciekawy efekt? Jeśli uruchomiłeś ten program na typowym komputerze domowym (rodzina i386) na ekranie zapewne pojawił Ci się taki oto napis:

Adres IP w postaci 32-bitowej zmiennej: 0101a8c0

Dlaczego jedynki są na początku zmiennej, skoro w programie były to dwa ostatnie bajty (pola c i d struktury)? Jest to problem kolejności bajtów. Aby dowiedzieć się o nim więcej przeczytaj rozdział przenośność programów. Zauważyłeś zatem, że za pomocą tego programu w prosty sposób zamieniliśmy cztery zmienne jednobajtowe w jedną czterobajtową. Jest to tylko jedno z możliwych zastosowań unii.

Inicjacja struktur i unii

[edytuj]

Jeśli tworzymy nową strukturę lub unię możemy zaraz po jej deklaracji wypełnić ją określonymi danymi. Rozważmy tutaj przykład:

 struct moja_struct {
    int a;
    char b;
    } moja = {1,'c'};

W zasadzie taka deklaracja nie różni się niczym od wypełnienia np. tablicy danymi. Jednak standard C99 wprowadza pewne udogodnienie zarówno przy deklaracji struktur, jak i unii. Polega ono na tym, że w nawiasie klamrowym możemy podać nazwy pól struktury lub unii którym przypisujemy wartość, np.:

 struct moja_struct {
    int a;
    char b;
    } moja = {.b = 'c'}; /* pozostawiamy pole a niewypełnione żadną konkretną wartością */

Wspólne własności typów wyliczeniowych, unii i struktur

[edytuj]

Warto zwrócić uwagę, że język C++ przy deklaracji zmiennych typów wyliczeniowych, unii lub struktur nie wymaga przed nazwą typu słowa kluczowego typedef. Na przykład poniższy kod jest poprawnym programem C++:

 enum   Enum   { A, B, C };
 union  Union  { int a; float b; };
 struct Struct { int a; float b; };
 int main() {
   Enum   e;
   Union  u;
   Struct s;
   e = A;
   u.a = 0;
   s.a = 0;
   return e + u.a + s.a;
 }

Nie jest to jednak poprawny kod C i należy o tym pamiętać szczególnie jeżeli uczysz się języka C korzystając z kompilatora C++.

Częstym idiomem w C jest użycie typedef od razu z definicją typu, by uniknąć pisania enum, union czy struct przy deklaracji zmiennych danego typu.[19]

 typedef struct struktura {
   int pole;
 } Struktura;

 
 Struktura s1;
 struct struktura s2;

W tym przypadku zmienne s1 i s2 są tego samego typu, który ma 2 nazwy: pełną:

struct struktura

i skróconą:

Struktura  



Możemy też zrezygnować z pełnej nazwy struktury i pozostawić tylko skróconą:

 typedef struct {
   int pole;
 } Struktura;

 Struktura s1;

Jeśli chcemy utworzyć strukturę rekurencyjną to wewnątrz struktury możemy użyć tylko nazwy długiej, nie krótkiej:

 typedef struct Wezel {// długa nazwa typu 
  double re;
  double im;
  int level;
  struct Wezel *poprzedni; /* poprzedni węzeł */     
} TWezel; // krótka nazwa typu

Należy również pamiętać, że po klamrze zamykającej definicje musi następować średnik. Brak tego średnika jest częstym błędem powodującym czasami niezrozumiałe komunikaty błędów. Jedynym wyjątkiem jest natychmiastowa definicja zmiennych danego typu, na przykład:

 struct Struktura {
   int pole;
 } s1, s2, s3;

lub też definicja nowego typu, jak w poprzednim bloku kodowym (TWezel).

Definicja typów wyliczeniowych, unii i struktur jest lokalna do bloku. To znaczy, możemy zdefiniować strukturę wewnątrz jednej z funkcji (czy wręcz wewnątrz jakiegoś bloku funkcji) i tylko tam będzie można używać tego typu.

Wskaźnik na unię i strukturę

[edytuj]

Podobnie, jak na każdą inną zmienna, wskaźnik może wskazywać także na unię lub strukturę. Oto przykład:

 typedef struct {
   int p1, p2;
 } Struktura;

 int main ()
 {
   Struktura s = { 0, 0 };
   Struktura *wsk = &s;
   wsk->p1 = 2;
   wsk->p2 = 3;
   return 0;
 }

Zapis wsk->p1 jest (z definicji) równoważny (*wsk).p1, ale bardziej przejrzysty i powszechnie stosowany. Wyrażenie wsk.p1 spowoduje błąd kompilacji (strukturą jest *wsk a nie wsk).


Zobacz też

[edytuj]

Pola bitowe

[edytuj]

Struktury mają pewne dodatkowe możliwości w stosunku do zmiennych. Mowa tutaj o rozmiarze elementu struktury. W przeciwieństwie do zmiennej może on mieć nawet 1 bit!. Aby móc zdefiniować taką zmienną musimy użyć tzw. pola bitowego. Wygląda ono tak:

 struct moja {
   unsigned int a1:4, /* 4 bity */
                a2:8, /* 8 bitów (często 1 bajt) */ 
                a3:1, /* 1 bit */
                a4:3; /* 3 bity */
 };

Wszystkie pola tej struktury mają w sumie rozmiar 16 bitów, jednak możemy odwoływać się do nich w taki sam sposób, jak do innych elementów struktury. W ten sposób efektywniej wykorzystujemy pamięć, jednak istnieją pewne zjawiska, których musimy być świadomi przy stosowaniu pól bitowych. Więcej na ten temat w rozdziale przenośność programów.

Pola bitowe znalazły zastosowanie głównie w implementacjach protokołów sieciowych.

Listy

[edytuj]

rodzaje list:

  • lista jednokierunkowa – w każdym elemencie listy jest przechowywane odniesienie tylko do jednego sąsiada (następnika lub poprzednika)
  • lista dwukierunkowa – w każdym elemencie listy jest przechowywane odniesienie zarówno do następnika, jak i poprzednika elementu w liście. Taka reprezentacja umożliwia swobodne przemieszczanie się po liście w obie strony
  • lista cykliczna – następnikiem ostatniego elementu jest pierwszy element. Po liście można więc przemieszczać się cyklicznie. Nie ma w takiej liście charakterystycznego ogona (ani głowy), często rozpoznawanego po tym, że jego następnik jest pusty (NULL)
  • lista z wartownikiem – lista z wyróżnionym elementem zwanym wartownikiem. Jest to specjalnie oznaczony element niewidoczny dla programisty wykorzystującego listę. Pusta lista zawiera wtedy tylko wartownika. Zastosowanie wartownika znacznie upraszcza implementację operacji na listach.


Studium przypadku: lista jednokierunkowa

[edytuj]

Rozważmy teraz coś, co każdy z nas może spotkać w codziennym życiu. Każdy z nas widział kiedyś jakiś przykład listy (czy to zakupów, czy też listę wierzycieli). Język C też oferuje listy, jednak w programowaniu listy będą służyły do czegoś innego. Wyobraźmy sobie sytuację, w której jesteśmy autorami genialnego programu, który znajduje kolejne liczby pierwsze. Oczywiście każdą kolejną liczbę pierwszą może wyświetlać na ekran, jednak z matematyki wiemy, że dana liczba jest liczbą pierwszą, jeśli nie dzieli się przez żadną liczbę pierwszą ją poprzedzającą, mniejszą od pierwiastka z badanej liczby. Mniej więcej chodzi o to, że moglibyśmy wykorzystać znalezione wcześniej liczby do przyspieszenia działania naszego programu. Jednak nasze liczby trzeba jakoś mądrze przechować w pamięci. Tablice mają ograniczenie - musimy z góry znać ich rozmiar. Jeśli zapełnilibyśmy tablicę, to przy znalezieniu każdej kolejnej liczby musielibyśmy:

  1. przydzielać nowy obszar pamięci o rozmiarze poprzedniego rozmiaru + rozmiar zmiennej, przechowującej nowo znalezioną liczbę
  2. kopiować zawartość starego obszaru do nowego
  3. zwalniać stary, nieużywany obszar pamięci
  4. w ostatnim elemencie nowej tablicy zapisać znalezioną liczbę.

Cóż, trochę tutaj roboty jest, a kopiowanie całej zawartości jednego obszaru w drugi jest czasochłonne. W takim przypadku możemy użyć listy. Tworząc listę możemy w prosty sposób przechować nowo znalezione liczby. Przy użyciu listy nasze postępowanie ograniczy się do:

  1. przydzielenia obszaru pamięci, aby przechować wartość obliczeń
  2. dodać do listy nowy element

Prawda, że proste? Dodatkowo, lista zajmuje w pamięci tylko tyle pamięci, ile potrzeba na aktualną liczbę elementów. Pusta tablica zajmuje natomiast tyle samo miejsca co pełna tablica.

Implementacja listy

[edytuj]

W języku C aby stworzyć listę musimy użyć struktur. Dlaczego? Ponieważ musimy przechować co najmniej dwie wartości:

  1. pewną zmienną (np. liczbę pierwszą z przykładu)
  2. wskaźnik na kolejny element listy

Przyjmijmy, że szukając liczb pierwszych nie przekroczymy możliwości typu unsigned long:

 typedef struct element {
   struct element *next; /* wskaźnik na kolejny element listy */
   unsigned long val; /* przechowywana wartość */
 } el_listy;

Zacznijmy zatem pisać nasz eksperymentalny program, do wyszukiwania liczb pierwszych. Pierwszą liczbą pierwszą jest liczba 2 Pierwszym elementem naszej listy będzie zatem struktura, która będzie przechowywała liczbę 2. Na co będzie wskazywało pole next? Ponieważ na początku działania programu będziemy mieć tylko jeden element listy, pole next powinno wskazywać na NULL. Umówmy się zatem, że pole next ostatniego elementu listy będzie wskazywało NULL - po tym poznamy, że lista się skończyła.

 #include <stdio.h>
 #include <stdlib.h>
 typedef struct element {
   struct element *next;
   unsigned long val;
 } el_listy;
 
 el_listy *first; /* pierwszy element listy */
  
 int main ()
 {
   unsigned long i = 3; /* szukamy liczb pierwszych w zakresie od 3 do 1000 */
   const unsigned long END = 1000;
   first = malloc (sizeof(el_listy));
   first->val = 2;
   first->next = NULL;
   for (;i<=END;++i) {
     /* tutaj powinien znajdować się kod, który sprawdza podzielność sprawdzanej liczby przez
        poprzednio znalezione liczby pierwsze oraz dodaje liczbę do listy w przypadku stwierdzenia,
        że jest ona liczbą pierwszą. */
     }
   wypisz_liste(first);
  return 0;
 }

Na początek zajmiemy się wypisywaniem listy. W tym celu będziemy musieli "odwiedzić" każdy element listy. Elementy listy są połączone polem next, aby przejrzeć listę użyjemy następującego algorytmu:

  1. Ustaw wskaźnik roboczy na pierwszym elemencie listy
  2. Jeśli wskaźnik ma wartość NULL, przerwij
  3. Wypisz element wskazywany przez wskaźnik
  4. Przesuń wskaźnik na element, który jest wskazywany przez pole next
  5. Wróć do punktu 2
 void wypisz_liste(el_listy *lista)
 {
   el_listy *wsk=lista;          /* 1 */
   while( wsk != NULL )          /* 2 */
     {
     printf ("%lu\n", wsk->val); /* 3 */
     wsk = wsk->next;            /* 4 */
     }                           /* 5 */
 }

Zastanówmy się teraz, jak powinien wyglądać kod, który dodaje do listy następny element. Taka funkcja powinna:

  1. znaleźć ostatni element (tj. element, którego pole next == NULL)
  2. przydzielić odpowiedni obszar pamięci
  3. skopiować w pole val w nowo przydzielonym obszarze znalezioną liczbę pierwszą
  4. nadać polu next ostatniego elementu listy wartość NULL
  5. w pole next ostatniego elementu listy wpisać adres nowo przydzielonego obszaru

Napiszmy zatem odpowiednią funkcję:

 void dodaj_do_listy (el_listy *lista, unsigned long liczba)
 {
   el_listy *wsk, *nowy;
   wsk = lista;
   while (wsk->next != NULL)          /* 1 */
     { 
     wsk = wsk->next; /* przesuwamy wsk aż znajdziemy ostatni element */
     }
   nowy = malloc (sizeof(el_listy));  /* 2 */
   nowy->val = liczba;                /* 3 */
   nowy->next = NULL;                 /* 4 */
   wsk->next = nowy;                  /* 5 */
 }

I... to już właściwie koniec naszej funkcji (warto zwrócić uwagę, że funkcja w tej wersji zakłada, że na liście jest już przynajmniej jeden element). Wstaw ją do kodu przed funkcją main. Został nam jeszcze jeden problem: w pętli for musimy dodać kod, który odpowiednio będzie "badał" liczby oraz w przypadku stwierdzenia pierwszeństwa liczby, będzie dodawał ją do listy. Ten kod powinien wyglądać mniej więcej tak:

 int jest_pierwsza(el_listy *lista, int liczba)
 {
   el_listy *wsk;
   wsk = lista;
   while (wsk != NULL) {
     if ((liczba % wsk->val)==0) return 0;  /* jeśli reszta z dzielenia liczby
                                               przez którąkolwiek z poprzednio
                                               znalezionych liczb pierwszych
                                               jest równa zero, to znaczy, że liczba
                                               ta nie jest liczbą pierwszą */
     wsk = wsk->next;
     }
   /* natomiast jeśli sprawdzimy wszystkie poprzednio znalezione liczby
      i żadna z nich nie będzie dzieliła liczby i,
      możemy liczbę i dodać do listy liczb pierwszych */
   return 1;
 }
 ...
 for (;i<=END;++i) {
   if (jest_pierwsza(first, i))
     dodaj_do_listy (first,i);
     }

Kod

[edytuj]

Podsumujmy teraz efekty naszej pracy. Oto cały kod naszego programu:

 #include <stdio.h>
 #include <stdlib.h>
 
 typedef struct element {
   struct element *next;
   unsigned long val;
 } el_listy;
 
 el_listy *first;
 
 void dodaj_do_listy (el_listy *lista, unsigned long liczba)
 {
   el_listy *wsk, *nowy;
   wsk = lista;
   while (wsk->next != NULL)
     { 
     wsk = wsk->next; /* przesuwamy wsk aż znajdziemy ostatni element */
     }
   nowy =(el_listy*) malloc (sizeof(el_listy));
   nowy->val = liczba;
   nowy->next = NULL;
   wsk->next = nowy; /* podczepiamy nowy element do ostatniego z listy */
 }
 
 void wypisz_liste(el_listy *lista)
 {
   el_listy *wsk=lista;
   while( wsk != NULL )
     {
     printf ("%lu\n", wsk->val);
     wsk = wsk->next;
     }
 }
 
 int jest_pierwsza(el_listy *lista, int liczba)
 {
   el_listy *wsk;
   wsk = lista;
   while (wsk != NULL) {
     if ((liczba%wsk->val)==0) return 0;
        wsk = wsk->next;
     }
     return 1;
 }
 
 int main ()
 {
   unsigned long i = 3; /* szukamy liczb pierwszych w zakresie od 3 do 1000 */
   const unsigned long END = 1000;
   first =(el_listy*) malloc (sizeof(el_listy));
   first->val = 2;
   first->next = NULL;
   for (;i!=END;++i) {
     if (jest_pierwsza(first, i))
       dodaj_do_listy (first, i);
       }
   wypisz_liste(first);
   return 0;
 }

Możemy jeszcze pomyśleć, jak można by wykonać usuwanie elementu z listy. Najprościej byłoby zrobić:

wsk->next = wsk->next->next

ale wtedy element, na który wskazywał wcześniej wsk->next przestaje być dostępny i zaśmieca pamięć. Trzeba go usunąć. Zauważmy, że aby usunąć element potrzebujemy wskaźnika do elementu go poprzedzającego (po to, by nie rozerwać listy). Popatrzmy na poniższą funkcję:

 void usun_z_listy(el_listy *lista, int element)
 {
   el_listy *wsk=lista;
   while (wsk->next != NULL)
   {
     if (wsk->next->val == element) /* musimy mieć wskaźnik do elementu poprzedzającego */
     {
       el_listy *usuwany=wsk->next; /* zapamiętujemy usuwany element */
       wsk->next = usuwany->next;   /* przestawiamy wskaźnik next by omijał usuwany element */
       free(usuwany);               /* usuwamy z pamięci */
     } 
     else 
     {
       wsk = wsk->next;             /* idziemy dalej tylko wtedy kiedy nie usuwaliśmy */
     }                              /* bo nie chcemy zostawić duplikatów */
   }
 }

Funkcja ta jest tak napisana, by usuwała z listy wszystkie wystąpienia danego elementu (w naszym programie nie ma to miejsca, ale lista jest zrobiona tak, że może trzymać dowolne liczby). Zauważmy, że wskaźnik wsk jest przesuwany tylko wtedy, gdy nie kasowaliśmy. Gdybyśmy zawsze go przesuwali, przegapilibyśmy element gdyby występował kilka razy pod rząd.

Funkcja ta działa poprawnie tylko wtedy, gdy nie chcemy usuwać pierwszego elementu. Można to poprawić - dodając instrukcję warunkową do funkcji lub dodając do listy "głowę" - pierwszy element nie przechowujący niczego, ale upraszczający operacje na liście. Zostawiamy to do samodzielnej pracy.


Cały powyższy przykład omawiał tylko jeden przypadek listy - listę jednokierunkową. Jednak istnieją jeszcze inne typy list, np. lista jednokierunkowa cykliczna, lista dwukierunkowa oraz dwukierunkowa cykliczna. Różnią się one od siebie tylko tym, że:

  • w przypadku list dwukierunkowych - w strukturze el_listy znajduje się jeszcze pole, które wskazuje na element poprzedni
  • w przypadku list cyklicznych - ostatni element wskazuje na pierwszy (nie rozróżnia się wtedy elementu pierwszego, ani ostatniego)

Lista dwukierunkowa

[edytuj]
//http://thradams.com/generator/linked_list.html
#include <stdlib.h>
#include <assert.h>
#include <errno.h>

struct book {
     char* title;
     struct book* next;
     struct book* prev;
};

void book_destroy(struct book* book) {
    /*remove if it is empty*/
     free(book->title);
}
 

struct books {
    struct book* head, *tail;
};

void books_insert_after(struct books* books, struct book* book, struct book* new_book)
{
    assert(books != NULL);
    assert(book != NULL);
    assert(new_book != NULL);
    assert(new_book->prev == NULL);
    assert(new_book->next == NULL);

    new_book->prev = book;

    if (book->next == NULL) {
        books->tail = new_book;
    }
    else {
        new_book->next = book->next;
        book->next->prev = new_book;
    }

    book->next = new_book;
}

void books_insert_before(struct books* books, struct book* book, struct book* new_book)
{
    assert(books != NULL);
    assert(book != NULL);
    assert(new_book != NULL);
    assert(new_book->prev == NULL);
    assert(new_book->next == NULL);

    new_book->next = book;
    if (book->prev == NULL) {
        books->head = new_book;
    }
    else {
        new_book->prev = book->prev;
        book->prev->next = new_book;
    }
    book->prev = new_book;

}
void books_remove(struct books* books, struct book* book)
{
    assert(books != NULL);
    assert(book != NULL);

    if (book->prev == NULL)
        books->head = book->next;
    else
        book->prev->next = book->next;

    if (book->next == NULL)
        books->tail = book->prev;
    else
        book->next->prev = book->prev;
    
    book_destroy(book);
    free(book);
}


void books_push_back(struct books* books, struct book* new_book)
{
   assert(books != NULL);
   assert(new_book != NULL);
   assert(new_book->prev == NULL);
   assert(new_book->next == NULL);

   if (books->tail == NULL) {
      books->head = new_book;
   }
   else {
      new_book->prev = books->tail;        
      books->tail->next = new_book;
   }
   books->tail = new_book;
}

void books_push_front(struct books* books, struct book* new_book)
{
    assert(books != NULL);
    assert(new_book != NULL);
     assert(new_book->prev == NULL);
    assert(new_book->next == NULL);

    if (books->head == NULL) {
        books->tail = new_book;
    }
    else {
        new_book->next = books->head;        
        books->head->prev = new_book;
    }
    books->head = new_book;
}

void books_destroy(struct books* books)
{
    //pre condition
    assert(books != NULL);

    struct book* it = books->head;
    while (it != NULL) {
        struct book* next = it->next;
        book_destroy(it);
        free(it);
        it = next;
    }
}

int main(int argc, char* argv[])
{
    struct books list = { 0 };
    struct book* b1 = calloc(1, sizeof(struct book));
    if (b1)
    {
        books_push_front(&list, b1);
    }
    books_destroy(&list);
}

Odnośniki

[edytuj]
  1. Błąd rozszerzenia cite: Błąd w składni znacznika <ref>; brak tekstu w przypisie o nazwie const
  2. Błąd rozszerzenia cite: Błąd w składni znacznika <ref>; brak tekstu w przypisie o nazwie null
  3. wikipedia: Null_(znak)
  4. wikipedia: Kod sterujący
  5. Szablon:Cite web
  6. Szablon:Cite web
  7. Szablon:Cite book
  8. Szablon:Cite web
  9. Szablon:Cite web
  10. 10,0 10,1 Szablon:Cite web
  11. Szablon:Cite web
  12. 12,0 12,1 Szablon:Cite web
  13. 13,0 13,1 Szablon:Cite web
  14. Szablon:Cite web
  15. c i unicode
  16. Błąd rozszerzenia cite: Błąd w składni znacznika <ref>; brak tekstu w przypisie o nazwie unicode
  17. stackoverflow question: how-to-properly-use-typedef-for-structs-in-c
  18. stackoverflow questions : passing-struct-to-function
  19. [Difference between 'struct' and 'typedef struct' in C++? ]



Tworzenie bibliotek

[edytuj]

Czym jest biblioteka

[edytuj]

Biblioteka[1] jest to zbiór funkcji, które zostały wydzielone po to, aby dało się z nich korzystać w wielu programach. Ułatwia to programowanie - nie musimy np. sami tworzyć funkcji printf. Każda biblioteka posiada swoje pliki nagłówkowe, które zawierają deklaracje funkcji bibliotecznych oraz często zawarte są w nich komentarze, jak używać danej funkcji. W tej części podręcznika nauczymy się tworzyć nasze własne biblioteki.

Pliki

[edytuj]

Biblioteka

  • składa się co najmniej z dwóch plików: jeden nagłówkowy (źródłowy, API) i jeden binarny (skompilowany)
  • zawiera funkcje (deklaracje w nagłówkowym i definicje w binarnym)
  • w postaci pakietu (ang. package) może dzielić się na pakiety dev i non-dev[2]

cechy

[edytuj]
  • jest przeznaczona do wykonania odrębnego zadania programistycznego
  • ma ściśle określony interfejs [3]
  • "Moduł ma charakter czarnej skrzynki (ang. black-box approach). Na zewnątrz modułu widoczne są wyłącznie te obiekty programistyczne, które tworzą interfejs. Natomiast sposób ich implementacji, jak i ewentualne obiekty pomocnicze są ukryte wewnątrz modułu."

Zasady budowy bibliotek ( modułów) wg strony wazniak.mimuw.edu.pl: [4]

  • powiązania między modułami powinny być jak najmniejsze;
  • jak najmniej szczegółów budowy jednego modułu miało wpływ na budowę innego modułu,
  • każdy moduł powinien koncentrować się wokół jednej decyzji projektowej, tzw. "sekretu" modułu, przy czym nie należy łączyć nie związanych ze sobą sekretów w jednym module; zasada ta jest znana pod nazwą separation of concerns,
  • użytkownicy modułów powinni polegać jedynie na tym, co jest określone w interfejsie i specyfikacji modułu, natomiast nie powinni polegać na żadnym konkretnym sposobie implementacji modułu, tzw. black-box approach.

typy

[edytuj]

wg


wg sposobu wykorzystania

[edytuj]
  • statyczne (ang. static library or statically-linked library) [5]
    • windows: .lib lub .obj
    • Unix: .a lub .o
  • dynamiczne[6]
    • biblioteka łączona dynamicznie,
      • Unix: biblioteka współdzielona (ang. shared library[7][8], shared object) .so, ścieżki poszukiwań plików bibliotek zapisane są w pliku /etc/ld.so.conf oraz w zmiennej środowiskowej $LD_LIBRARY_PATH.
      • Windows: .dll
    • biblioteki ładowane dynamicznie



wg autora

[edytuj]
  • standardowe - biblioteka zawierająca podstawowe funkcje, dostarczana wraz z kompilatorem lub interpreterem danego języka programowania. Nie wymagaja osobnej instalacji
  • niestandardowe, zewnętrzne, wymagajace osobnej instalacji

wg rozszerzenie

[edytuj]
  • a - biblioteki linkowane statycznie
  • bin - pliki binarne
  • fw - pliki firmware : biblioteki lub sterowniki sprzętu
  • ko - pliki dla modułów jądra
  • o - pliki obiektowe, np. ładowalne moduły jądra
  • so - dynamicznie linkowane biblioteki dzielone[9]

Ścieżka wyszukiwania

[edytuj]

Gcc w systemie Linuks wyszukuje nagłówki #include <file> w kilku standardowych katalogach: [10][11]

 /usr/local/include
 libdir/gcc/target/version/include
 /usr/target/include
 /usr/include

Możemy to sprawdzić za pomocą przełączników przy kompilacji:

gcc c.c -v -c 

przykładowy wynik:

ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/4.8/include
/usr/local/include
/usr/lib/gcc/x86_64-linux-gnu/4.8/include-fixed
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.

Zmienna środowiskowa LD_LIBRARY_PATH zawiera ścieżki do katalogów bibliotek. Te katalogi kompilator będzie przeszukiwał w trakcie kompilacji:[12]

LD_LIBRARY_PATH

W celu dopisania ścieżki do własnej biblioteki ustaw wartość zmiennej środowiskowej, na przykład w konsoli wpisz:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib32:/usr/lib32

gdzie /lib32 i /usr/lib32 są dopisywanymi ścieżkami do bibliotek (plików *.so). Jak widzimy w przykładzie, ścieżki oddzielamy dwukropkiem.


Sprawdzamy jakie ścieżki są przeszukiwane:[13]

 echo | gcc -Wp,-v -x c++ - -fsyntax-only

przykładowy wynik:

ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/4.8"
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/4.8
 /usr/include/x86_64-linux-gnu/c++/4.8
 /usr/include/c++/4.8/backward
 /usr/lib/gcc/x86_64-linux-gnu/4.8/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/4.8/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

Zależności

[edytuj]
  • ldd (
  • wyszukiwanie w kodzie [14]

Więcej o kompilowaniu

[edytuj]

Program make

[edytuj]

Dość często może się zdarzyć, że nasz program składa się z kilku plików źródłowych. Jeśli tych plików jest mało (np. 3-5) możemy jeszcze próbować ręcznie kompilować każdy z nich. Jednak jeśli tych plików jest dużo, lub chcemy pokazać nasz program innym użytkownikom musimy stworzyć elegancki sposób kompilacji naszego programu. Właśnie po to, aby zautomatyzować proces kompilacji powstał program make. Program make analizuje pliki Makefile i na ich podstawie wykonuje określone czynności.

Budowa pliku Makefile

[edytuj]


Najważniejszym elementem pliku Makefile są zależności oraz reguły przetwarzania. Zależności polegają na tym, że np. jeśli nasz program ma być zbudowany z 4 plików, to najpierw należy skompilować każdy z tych 4 plików, a dopiero później połączyć je w jeden cały program. Zatem zależności określają kolejność wykonywanych czynności. Natomiast reguły określają jak skompilować dany plik. Zależności tworzy się tak:

co: od_czego
  reguły...

lub innymi słowami:

 co: z_czego
   jak

Dzięki temu program make zna już kolejność wykonywanych działań oraz czynności, jakie ma wykonać. Aby zbudować "co" należy wykonać polecenie: make co. Pierwsza reguła w pliku Makefile jest regułą domyślną. Jeśli wydamy polecenie make bez parametrów, zostanie zbudowana właśnie reguła domyślna. Tak więc dobrze jest jako pierwszą regułę wstawić regułę budującą końcowy plik wykonywalny; zwyczajowo regułę tą nazywa się all.

Należy pamiętać, by sekcji "co" nie wcinać, natomiast "reguły" wcinać tabulatorem. Część "od_czego" może być pusta.


Plik Makefile umożliwia też definiowanie pewnych zmiennych. Nie trzeba tutaj się już troszczyć o typ zmiennej, wystarczy napisać:

nazwa_zmiennej = wartość

W ten sposób możemy zadeklarować dowolnie dużo zmiennych. Zmienne mogą być różne - nazwa kompilatora, jego parametry i wiele innych. Zmiennej używamy w następujący sposób: $(nazwa_zmiennej).

Komentarze w pliku Makefile tworzymy zaczynając linię od znaku hash (#).

Przykładowy plik Makefile

[edytuj]

Dość tej teorii, teraz zajmiemy się działającym przykładem. Załóżmy, że nasz przykładowy program nazywa się test oraz składa się z czterech plików:

pierwszy.c
drugi.c
trzeci.c
czwarty.c

oraz plików nagłówkowych

pierwszy.h
drugi.h
trzeci.h
czwarty.h

przyjmijmy, iż plik drugi.c w treści ma wpisane dołączenie plików:

  1. include "trzeci.h"
  2. include "czwarty.h"

Odpowiedni plik Makefile z użyciem kompilacji 2 etapowej powinien wyglądać mniej więcej tak:

 # Mój plik makefile - wpisz 'make all' aby skompilować cały program
 # (właściwie wystarczy wpisać 'make' - all jest domyślny jako pierwszy cel)
 CC = gcc  
 
 all: pierwszy.o drugi.o trzeci.o czwarty.o
   $(CC) pierwszy.o drugi.o trzeci.o czwarty.o -o test
 
 pierwszy.o: pierwszy.c pierwszy.h
   $(CC) pierwszy.c -c -o pierwszy.o
 
 drugi.o: drugi.c drugi.h trzeci.h czwarty.h
   $(CC) drugi.c -c -o drugi.o
 
 trzeci.o: trzeci.c trzeci.h
   $(CC) trzeci.c -c -o trzeci.o
 
 czwarty.o: czwarty.c
   $(CC) czwarty.c -c -o czwarty.o


Widzimy, że nasz program zależy od 4 plików z rozszerzeniem .o (pierwszy.o itd.), potem każdy z tych plików zależy od plików .c, które program make skompiluje w pierwszej kolejności, a następnie połączy w jeden program (test). Nazwę kompilatora zapisaliśmy jako zmienną, ponieważ powtarza się i zmienna jest sposobem, by zmienić ją wszędzie za jednym zamachem. dodanie jako zależności plików z rozszerzeniem .h zapewnia rekompilację plików w których są używane zdefiniowane w nich wartości. Brak tych wpisów jest najczęstszą przyczyną braku zmian działania programu po zmianie ustawień w plikach nagłówkowych.

Zatem jak widać używanie pliku Makefile jest bardzo proste. Warto na koniec naszego przykładu dodać regułę, która wyczyści katalog z plików .o:

clean:
  rm -f *.o test

Ta reguła spowoduje usunięcie wszystkich plików .o oraz naszego programu jeśli napiszemy make clean.

Możemy też ukryć wykonywane komendy albo dopisać własny opis czynności:

clean:
  @echo Usuwam gotowe pliki
  @rm -f *.o test

Ten sam plik Makefile mógłby wyglądać inaczej:

 CFLAGS = -g -O # tutaj można dodawać inne flagi kompilatora
 LIBS = -lm # tutaj można dodawać biblioteki
 
 OBJ =\
     pierwszy.o \
     drugi.o \
     trzeci.o \
     czwarty.o
 
 all: main
 
 clean:
        rm -f *.o test
 .c.o:
        $(CC) -c $(INCLUDES) $(CFLAGS) $<
 
 main: $(OBJ)
        $(CC) $(OBJ) $(LIBS) -o test

Tak naprawdę jest to dopiero bardzo podstawowe wprowadzenie do używania programu make, jednak jest ono wystarczające, byś zaczął z niego korzystać. Wyczerpujące omówienie całego programu niestety przekracza zakres tego podręcznika.


Wzorcowy plik Makefile

[edytuj]

Zaletą tego pliku makefile jest to, że automatycznie określa on zależności. Wszystko, co musisz zrobić, to umieścić pliki C/C++ w folderze src/.[15]


# Thanks to Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/)
TARGET_EXEC := final_program

BUILD_DIR := ./build
SRC_DIRS := ./src

# Find all the C and C++ files we want to compile
# Note the single quotes around the * expressions. The shell will incorrectly expand these otherwise, but we want to send the * directly to the find command.
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')

# Prepends BUILD_DIR and appends .o to every src file
# As an example, ./your_dir/hello.cpp turns into ./build/./your_dir/hello.cpp.o
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)

# String substitution (suffix version without %).
# As an example, ./build/hello.cpp.o turns into ./build/hello.cpp.d
DEPS := $(OBJS:.o=.d)

# Every folder in ./src will need to be passed to GCC so that it can find header files
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
# Add a prefix to INC_DIRS. So moduleA would become -ImoduleA. GCC understands this -I flag
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

# The -MMD and -MP flags together generate Makefiles for us!
# These files will have .d instead of .o as the output.
CPPFLAGS := $(INC_FLAGS) -MMD -MP

# The final build step.
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
	$(CXX) $(OBJS) -o $@ $(LDFLAGS)

# Build step for C source
$(BUILD_DIR)/%.c.o: %.c
	mkdir -p $(dir $@)
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# Build step for C++ source
$(BUILD_DIR)/%.cpp.o: %.cpp
	mkdir -p $(dir $@)
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@


.PHONY: clean
clean:
	rm -r $(BUILD_DIR)

# Include the .d makefiles. The - at the front suppresses the errors of missing
# Makefiles. Initially, all the .d files will be missing, and we don't want those
# errors to show up.
-include $(DEPS)

Optymalizacje

[edytuj]

Kompilator GCC umożliwia generację kodu zoptymalizowanego dla konkretnej architektury. Służą do tego opcje -march= i -mtune=. Stopień optymalizacji ustalamy za pomocą opcji -Ox, gdzie x jest numerem stopnia optymalizacji (od 1 do 3). Możliwe jest też użycie opcji -Os, która powoduje generowanie kodu o jak najmniejszym rozmiarze. Aby skompilować dany plik z optymalizacjami dla procesora Athlon XP, należy napisać tak:

gcc program.c -o program -march=athlon-xp -O3

Z optymalizacjami należy uważać, gdyż często zdarza się, że kod skompilowany bez optymalizacji działa zupełnie inaczej, niż ten, który został skompilowany z optymalizacjami.

Wyrównywanie

[edytuj]

Wyrównywanie jest pewnym zjawiskiem, na które w bardzo wielu podręcznikach, mówiących o C w ogóle się nie wspomina. Ten rozdział ma za zadanie wyjaśnienie tego zjawiska oraz uprzedzenie programisty o pewnych faktach, które w późniejszej jego "twórczości" mogą zminimalizować czas na znalezienie pewnych informacji, które mogą wpływać na to, że jego program nie będzie działał poprawnie.

Często zdarza się, że kompilator w ramach optymalizacji "wyrównuje" elementy struktury tak, aby procesor mógł łatwiej odczytać i przetworzyć dane. Przyjrzyjmy się bliżej następującemu fragmentowi kodu:

 typedef struct {
   unsigned char wiek; /* 8 bitów */
   unsigned int dochod; /* 32 bity */
   unsigned char plec; /* 8 bitów */
   unsigned char wzrost; /* 8 bitów */
 } nasza_str;

Aby procesor mógł łatwiej przetworzyć dane kompilator może dodać do tej struktury jedno, ośmiobitowe pole. Wtedy struktura będzie wyglądała tak:

 typedef struct {
   unsigned char wiek; /*8 bitów */
   unsigned char fill[1]; /* 8 bitów */
   unsigned int dochod; /* 32 bity */
   unsigned char plec; /* 8 bitów */
   unsigned char wzrost; /* 8 bitów */
 } nasza_str;

Wtedy rozmiar zmiennych przechowujących wiek, wzrost, płeć, oraz dochód będzie wynosił 64 bity - będzie zatem potęgą liczby dwa i procesorowi dużo łatwiej będzie tak ułożoną strukturę przechowywać w pamięci cache. Jednak taka sytuacja nie zawsze jest pożądana. Może się okazać, że nasza struktura musi odzwierciedlać np. pojedynczy pakiet danych, przesyłanych przez sieć. Nie może być w niej zatem żadnych innych pól, poza tymi, które są istotne do transmisji. Aby wymusić na kompilatorze wyrównanie 1-bajtowe (co w praktyce wyłącza je) należy przed definicją struktury dodać dwie linijki. Ten kod działa pod Visual C++:

 #pragma pack(push)
 #pragma pack(1)
 
 struct struktura { /*...*/ };
 
 #pragma pack(pop)

W kompilatorze GCC, należy dodać po deklaracji struktury przed średnikiem kończącym jedną linijkę:

__attribute__ ((packed))

Działa ona dokładnie tak samo, jak makra #pragma, jednak jest ona obecna tylko w kompilatorze GCC.

Dzięki użyciu tego atrybutu, kompilator zostanie "zmuszony" do braku ingerencji w naszą strukturę. Jest jednak jeszcze jeden, być może bardziej elegancki sposób na obejście dopełniania. Zauważyłeś, że dopełnienie, dodane przez kompilator pojawiło się między polem o długości 8 bitów (wiek) oraz polem o długości 32 bitów (dochod). Wyrównywanie polega na tym, że dana zmienna powinna być umieszczona pod adresem będącym wielokrotnością jej rozmiaru. Oznacza to, że jeśli np. mamy w strukturze na początku dwie zmienne, o rozmiarze jednego bajta, a potem jedną zmienną, o rozmiarze 4 bajtów, to pomiędzy polami o rozmiarze 2 bajtów, a polem czterobajtowym pojawi się dwubajtowe dopełnienie. Może Ci się wydawać, że jest to tylko niepotrzebne mącenie w głowie, jednak niektóre architektury (zwłaszcza typu RISC) mogą nie wykonać kodu, który nie został wyrównany. Dlatego, naszą strukturę powinniśmy zapisać mniej więcej tak:

 typedef struct {
   unsigned int dochod; /* 32 bity */
   unsigned char wiek; /* 8 bitów */
   unsigned char plec; /* 8 bitów */
 } nasza_str;

W ten sposób wyrównana struktura nie będzie podlegała modyfikacjom przez kompilator oraz będzie przenośna pomiędzy różnymi kompilatorami.

Wyrównywanie działa także na pojedynczych zmiennych w programie, jednak ten problem nie powoduje tyle zamieszania, co ingerencja kompilatora w układ pól struktury. Wyrównywanie zmiennych polega tylko na tym, że kompilator umieszcza je pod adresami, które są wielokrotnością ich rozmiaru

Kompilacja krzyżowa

[edytuj]

Mając w domu dwa komputery, o odmiennych architekturach (np. i386 oraz Sparc) możemy potrzebować stworzyć program dla jednej maszyny, mając do dyspozycji tylko drugi komputer. Nie musimy wtedy latać do znajomego, posiadającego odpowiedni sprzęt. Możemy skorzystać z tzw. kompilacji krzyżowej (ang. cross-compile). Polega ona na tym, że program nie jest kompilowany pod procesor, na którym działa kompilator, lecz na inną, zdefiniowaną wcześniej maszynę. Efekt będzie taki sam, a skompilowany program możemy bez problemu uruchomić na drugim komputerze.

Inne narzędzia

[edytuj]

Wśród przydatnych narzędzi, warto wymienić również :


Objdump służy do deasemblacji i analizy skompilowanych programów. Readelf służy do analizy pliku wykonywalnego w formacie ELF (używanego w większości systemów z rodziny Unix). Więcej informacji możesz uzyskać, pisząc (w systemach Unix):

man 1 objdump
man 1 readelf

Zobacz również

[edytuj]


Zaawansowane operacje matematyczne

[edytuj]

Biblioteki

[edytuj]
  • Biblioteka math.h
  • complex.h - liczby zespolone
  • liczby rzeczywiste
    • float.h - liczny zmiennoprzecinkowe
    • fenv.h - środowiski zmiennoprzecinkowe
  • liczby całkowite
    • limits.h - charakterystyka typów liczb calkowitych
    • inittypes.h - konwersja liczb całkowitych
    • stdckdiniot.h - kontrola arytmetyki liczb całkowitych
  • atomic.h - typy atomowe
  • stdbit.h - marzędzia bitowe i bajtowe
  • stdbool.h - wartości i typy logiczne
  • tgmath.h - funkcje matematyczne niezależne od typu


Liczby

[edytuj]
Liczby 0–F16 w systemie
dziesiętnym, ósemkowym i dwójkowym
  0hex   =   0dec   =   0oct     0     0     0     0  
1hex = 1dec = 1oct 0 0 0 1
2hex = 2dec = 2oct 0 0 1 0
3hex = 3dec = 3oct 0 0 1 1
4hex = 4dec = 4oct 0 1 0 0
5hex = 5dec = 5oct 0 1 0 1
6hex = 6dec = 6oct 0 1 1 0
7hex = 7dec = 7oct 0 1 1 1
8hex = 8dec = 10oct 1 0 0 0
9hex = 9dec = 11oct 1 0 0 1
Ahex = 10dec = 12oct 1 0 1 0
Bhex = 11dec = 13oct 1 0 1 1
Chex = 12dec = 14oct 1 1 0 0
Dhex = 13dec = 15oct 1 1 0 1
Ehex = 14dec = 16oct 1 1 1 0
Fhex = 15dec = 17oct 1 1 1 1

Rodzaje liczb

[edytuj]

Dodatkowo rodzaj liczby definiują :


Jak widać nie ma :

  • Liczb niewymiernych ( chyba że korzystamy z obliczeń symbolicznych, np. program Maxima CAS )

liczby binarne

[edytuj]

Sposoby :


Binarna stała całkowita z użyciem rozszerzenia gcc [19]

  • prefix ‘0b’ lub ‘0B’
  • sekwencja cyfr ‘0’ lub ‘1’
  • suffix ‘L’ lub ‘UL’

Następujące zapisy są identyczne:

     i =       42; // decimal
     i =     0x2a; // hexadecimal
     i =      052; // octal
     i = 0b101010; // binary


int i = 1 << 9; /* binary 1 followed by 9 binary 0's */ [20]


Liczby szestnastkowe ( hexadecymalne)

[edytuj]

Liczba szesnastkowa:

  • jest poprzedzana przez przedrostek „0x” lub „0X”
  • jest wartością całkowitą i można ją przechowywać w typie całkowitym typów danych (char, short lub int)


Przykłady:

  • „0x64” odpowiada 100 w systemie dziesiętnym
  • wartość szesnastkowa 3E8 jest reprezentowana w c jako 0x3E8

Każdy bajt ( 8 bitów) to 2-cyfrowa liczba szesnastkowa


Maksymalna liczba całkowita bez znaku zajmująca n bajtów:

  • 1 bajt = 8 bitów:
  • 2 bajty = 16 bitów
  • 4 bajty = 32 bity
  • 8 bajtów = 64 bity

Prezentacja liczb rzeczywistych w pamięci komputera

[edytuj]

Liczby rzeczywisty

[edytuj]
Format liczby podwójnej precyzji

W wielu książkach nie ma w ogóle tego tematu. Być może ten temat może wydać Ci się niepotrzebnym, lecz dzięki niemu zrozumiesz, jak komputer radzi sobie z przecinkiem [21]oraz dlaczego niektóre obliczenia dają niezbyt dokładne wyniki.[22]
Na początek trochę teorii. Do przechowywania liczb rzeczywistych przeznaczone są 3 typy: float, double oraz long double. Zajmują one odpowiednio 32, 64 oraz 80 bitów. Wiemy też, że komputer nie ma fizycznej możliwości zapisania przecinka. Spróbujmy teraz zapisać jakąś liczbę wymierną w formie liczb binarnych. Nasza liczba to powiedzmy 4.25. Spróbujmy ją rozbić na sumę potęg dwójki: 4 = 1*22 + 0*21+0*20. Dobra - rozpisaliśmy liczbę 4, ale co z częścią dziesiętną? Skorzystajmy z zasad matematyki - 0.25 = 2-2. Zatem nasza liczba powinna wyglądać tak:

100.01

Ponieważ komputer nie jest w stanie przechować pozycji przecinka, ktoś wpadł na prosty ale sprytny pomysł ustawienia przecinka jak najbliżej początku liczby i tylko mnożenia jej przez odpowiednią potęgę dwójki. Taki sposób przechowywania liczb nazywamy zmiennoprzecinkowym, a proces przekształcania naszej liczby z postaci czytelnej przez człowieka na format zmiennoprzecinkowy nazywamy normalizacją. Wróćmy do naszej liczby - 4.25. W postaci binarnej wygląda ona tak: 100.01, natomiast po normalizacji będzie wyglądała tak: 1.0001*22. W ten sposób w pamięci komputera znajdą się dwie informacje: liczba zakodowana w pamięci z "wirtualnym" przecinkiem oraz numer potęgi dwójki. Te dwie informacje wystarczają do przechowania wartości liczby. Jednak pojawia się inny problem - co się stanie, jeśli np. będziemy chcieli przełożyć liczbę typu ? Otóż tutaj wychodzą na wierzch pewne niedociągnięcia komputera w dziedzinie samej matematyki. 1/3 daje w rozwinięciu dziesiętnym 0.(3). Jak zatem zapisać taką liczbę? Otóż nie możemy przechować całego jej rozwinięcia (wynika to z ograniczeń typu danych - ma on niestety skończoną liczbę bitów). Dlatego przechowuje się tylko pewne przybliżenie liczby. Jest ono tym bardziej dokładne im dany typ ma więcej bitów. Zatem do obliczeń wymagających dokładnych danych powinniśmy użyć typu double lub long double. Na szczęście w większości przeciętnych programów tego typu problemy zwykle nie występują. A ponieważ początkujący programista nie odpowiada za tworzenie programów sterujących np. lotem statku kosmicznego, więc drobne przekłamania na odległych miejscach po przecinku nie stanowią większego problemu.

Program wyświetlający wewnętrzną reprezentację liczby podwójnej precyzji:[23][24]

#include <stdio.h>

int main(void)
{
  double a = 1.0 / 3;
  size_t i;
  size_t iMax= sizeof a;

  printf("bytes are numbered from 0 to %x\n", (unsigned)iMax -1);

  for (i = 0; i < iMax ; ++i) 
  {
    printf("byte number %u  =  %x\n", (unsigned)i, ((unsigned char *)&a)[i]);
  }


  printf("hex memory representation  of %f is : \n", a);
  for (i = iMax; i>0 ; --i) 
  {
    printf("%x", ((unsigned char *)&a)[i-1]);
  }
  printf(" \n");
  return 0;
}

Daje wynik:

bytes are numbered from 0 to 7
byte number 0  =  55
byte number 1  =  55
byte number 2  =  55
byte number 3  =  55
byte number 4  =  55
byte number 5  =  55
byte number 6  =  d5
byte number 7  =  3f

hex memory representation  of 0.333333 is : 
3fd5555555555555


0 01111111101 01010101010101010101010101010101010101010101010101012 = 3FD5 5555 5555 555516 ≙ +2−2 × (1 + 2−2 + 2−4 + ... + 2−52) ≈ 1/3


Given the hexadecimal representation 3FD5 5555 5555 555516,
  Sign = 0
  Exponent = 3FD16 = 1021
  Exponent Bias = 1023 (constant value; see above)
  Fraction = 5 5555 5555 555516
  Value = 2(Exponent − Exponent Bias) × 1.Fraction – Note that Fraction must not be converted to decimal here
        = 2−2 × (15 5555 5555 555516 × 2−52)
        = 2−54 × 15 5555 5555 555516
        = 0.333333333333333314829616256247390992939472198486328125
        ≈ 1/3



Zobacz również:

Liczby całkowite

[edytuj]

Liczba całkowitej bez znaku (ang. unsigned char lub uint8_t ) zajmującej 1 bajt, czyli 8 bitów.


Ta tabela ilustruje wartość

 

Oznaczenia

  • MSB = najbardziej znaczący bit
  • LSB = najmniej znaczący bit
Liczba dziesiętna bez znaku 149 w postaci binarnej z zaznaczonym LSB
Liczba dziesiętna bez znaku 149 w postaci binarnej z zaznaczonym MSB

Bit position i 7 6 5 4 3 2 1 0
Binary digit 1 0 0 1 0 1 0 1
Bit weight = ( 2i ) 27 26 25 24 23 22 21 20
Bit position label MSB LSB

Liczba całkowita bez znaku zajmująca 2 bajty = 16 bitów ( uint16_t )


Wartość

  • dziesiętna (decymalna) = 769
  • dwójkowa (binarna) = 1100000001
  • hexadecymalna = 0x0301


 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |  
 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+


Zadania:

  • konwersja dwóch wartości 8-bitowych na jedną wartość 16-bitową w C[26]
  • dzielenie całkowitej liczby 16 bitowej bez znaku na 2 bajty[27]

Konwersje liczb

[edytuj]
/*

gcc b.c -Wall -Wextra


*/


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


/*
https://stackoverflow.com/questions/5488377/converting-an-integer-to-binary-in-c


If you want to transform a number into another number (not number to string of characters), and you can do with a small range (0 to 1023 for implementations with 32-bit integers), you don't need to add char* to the solution. pmg

*/
unsigned unsigned_int_to_bin(unsigned int k) {
    if (k == 0) return 0;
    if (k == 1) return 1;                       /* optional */
    return (k % 2) + 10 * unsigned_int_to_bin(k / 2);
}



int main(void){


	unsigned int u = 1019; 
	if (u>1023) {fprintf(stderr, "input number decimal u = %d is too big; close the program ! \n", u); return 1;}
	
	unsigned int b = unsigned_int_to_bin(u);
	fprintf(stdout, "decimal u = %d as a binary unsigned number = %d\n", u, b);
	



	return 0;

}

Wynik:

gcc b.c -Wall -Wextra
./a.out
decimal u = 1019 as a binary unsigned number = 1111111011



Konwersja zapisu matematycznego na program komputerowy w języku C

[edytuj]

sumowanie

[edytuj]

W zapisie matematycznym do przedstawiania w zwarty sposób sumowania wielu podobnych wyrazów ( serii) stosuje się symbol sumowania , wywodzący się z wielkiej greckiej litery sigma. Jest on zdefiniowany jako:

gdzie

  • oznacza indeks sumowania
  • to zmienna przedstawiająca każdy kolejny wyraz w szeregu
  • to dolna granica sumowania
  • to górna granica sumowania.

Wyrażenie „i = m” pod symbolem sumowania oznacza, że indeks rozpoczyna się od wartości Następnie dla każdego kolejnego wyrazu indeks jest zwiększany o 1, aż osiągnie wartość (tj. ), który jest ostatnim wyrazem sumowania.


#include <stdio.h>

int summation(const int m,  const int n )
{
   int s = 0;
   for(int i=m; i<=n; ++i)
   {
     s += i;
   }
   return s;
}



int main()
{
   int sum;
   int m = 1; // lower index
   int n = 100; // upper index
   
   sum = summation(m,n);
   
   printf("sum of integer numbers from %d to %d is %d\n", m, n, sum);
   return 0; 
 }
 gcc s.c -Wall -Wextra
 ./a.out
 sum of integer numbers from 1 to 100 is 5050


Suma sum:

Sumy są obliczane od prawej do lewej. Oznacza to, że sigma po lewej stronie jest najwyżej zagnieżdżoną pętlą: [28]

    
sum = 0;
for (i =1; i<=4; i++)
    for (j=1; j <=2; j++)
         sum += i * j* j;

produkt ( iloczyn)

[edytuj]

W zapisie matematycznym do przedstawiania w zwarty sposób iloczynu wielu podobnych wyrazów ( serii) stosuje się symbol iloczynu , wywodzący się z wielkiej greckiej litery pi. Jest on zdefiniowany jako:


#include <stdio.h>

int product(const int m,  const int n )
{
   int p = 1;
   for(int i=m; i<=n; ++i)
   {
     p *= i;
   }
   return p;
}



int main()
{
   int p;
   int m = 1; // lower index
   int n = 10; // upper index
   
    p = product(m,n);
   
   printf("product of integer numbers from %d to %d is %d\n", m, n, p);
   
}

Dla zakresu [1,10] program działa poprawnie

gcc p.c -Wall -Wextra
./a.out
product of integer numbers from 1 to 10 is 3628800

Dla zakresu [1,100] program daje dziwny wynik:

gcc p.c -Wall -Wextra
./a.out
product of integer numbers from 1 to 100 is 0


Wartość iloczynu możemy obliczyć za pomoca wolfram alfa.

Product[k, {k, 1, 100}] = 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

Liczba ma 159 cyfr, to więcej niż zakres dostępnych standardowych liczb całkowitych. Przekroczenie zakresu powoduje błędny wynik.

Jeśli dolny zakres wynosi 1 to wynik jest równy silni ( ang factorial). Wartość silnii rośnie szybciej niż wzrost funkcji wykładniczej.


Sprawdzenie czy nie będzie przekroczenia zakresu liczb powinniśmy wykonać przed wykonaniem działania:

#include <stdio.h>
#include <limits.h> // INT_MAX
/*
http://c-faq.com/misc/sd26.html
functions for ``careful'' multiplication.

*/

int chkmul(int a, int b)
{
	int sign = 1;
	if(a == 0 || b == 0) return 0;
	if(a < 0) { a = -a; sign = -sign; }
	if(b < 0) { b = -b; sign = -sign; }
	if(INT_MAX / b < a) {
		fprintf(stdout,"int overflow\t");
		return (sign > 0) ? INT_MAX : INT_MIN;
	}
	
	
	return sign*a*b;
}


int product(const int m,  const int n )
{
   int p = 1;
   for(int i=m; i<=n; ++i)
   {
     fprintf(stdout,"i = %d \t", i);
     p = chkmul(p,i);
     fprintf(stdout,"p = %d \n", p);
     
   }
   return p;
}



int main()
{
   int p;
   int m = 1; // lower index
   int n = 100; // upper index
   
    p = product(m,n);
   
   printf("product of integer numbers from %d to %d is %d\n", m, n, p);
   
}


gcc p.c -Wall -Wextra
./a.out
i = 1 	p = 1 
i = 2 	p = 2 
i = 3 	p = 6 
i = 4 	p = 24 
i = 5 	p = 120 
i = 6 	p = 720 
i = 7 	p = 5040 
i = 8 	p = 40320 
i = 9 	p = 362880 
i = 10 	p = 3628800 
i = 11 	p = 39916800 
i = 12 	p = 479001600 
i = 13 	int overflow	p = 2147483647 
i = 14 	int overflow	p = 2147483647 
i = 15 	int overflow	p = 2147483647 
i = 16 	int overflow	p = 2147483647 
i = 17 	int overflow	p = 2147483647 
i = 18 	int overflow	p = 2147483647 
i = 19 	int overflow	p = 2147483647 
i = 20 	int overflow	p = 2147483647 
i = 21 	int overflow	p = 2147483647 
i = 22 	int overflow	p = 2147483647 
i = 23 	int overflow	p = 2147483647 
i = 24 	int overflow	p = 2147483647 
i = 25 	int overflow	p = 2147483647 
i = 26 	int overflow	p = 2147483647 
i = 27 	int overflow	p = 2147483647 
i = 28 	int overflow	p = 2147483647 
i = 29 	int overflow	p = 2147483647 
i = 30 	int overflow	p = 2147483647 
i = 31 	int overflow	p = 2147483647 
i = 32 	int overflow	p = 2147483647 
i = 33 	int overflow	p = 2147483647 
i = 34 	int overflow	p = 2147483647 
i = 35 	int overflow	p = 2147483647 
i = 36 	int overflow	p = 2147483647 
i = 37 	int overflow	p = 2147483647 
i = 38 	int overflow	p = 2147483647 
i = 39 	int overflow	p = 2147483647 
i = 40 	int overflow	p = 2147483647 
i = 41 	int overflow	p = 2147483647 
i = 42 	int overflow	p = 2147483647 
i = 43 	int overflow	p = 2147483647 
i = 44 	int overflow	p = 2147483647 
i = 45 	int overflow	p = 2147483647 
i = 46 	int overflow	p = 2147483647 
i = 47 	int overflow	p = 2147483647 
i = 48 	int overflow	p = 2147483647 
i = 49 	int overflow	p = 2147483647 
i = 50 	int overflow	p = 2147483647 
i = 51 	int overflow	p = 2147483647 
i = 52 	int overflow	p = 2147483647 
i = 53 	int overflow	p = 2147483647 
i = 54 	int overflow	p = 2147483647 
i = 55 	int overflow	p = 2147483647 
i = 56 	int overflow	p = 2147483647 
i = 57 	int overflow	p = 2147483647 
i = 58 	int overflow	p = 2147483647 
i = 59 	int overflow	p = 2147483647 
i = 60 	int overflow	p = 2147483647 
i = 61 	int overflow	p = 2147483647 
i = 62 	int overflow	p = 2147483647 
i = 63 	int overflow	p = 2147483647 
i = 64 	int overflow	p = 2147483647 
i = 65 	int overflow	p = 2147483647 
i = 66 	int overflow	p = 2147483647 
i = 67 	int overflow	p = 2147483647 
i = 68 	int overflow	p = 2147483647 
i = 69 	int overflow	p = 2147483647 
i = 70 	int overflow	p = 2147483647 
i = 71 	int overflow	p = 2147483647 
i = 72 	int overflow	p = 2147483647 
i = 73 	int overflow	p = 2147483647 
i = 74 	int overflow	p = 2147483647 
i = 75 	int overflow	p = 2147483647 
i = 76 	int overflow	p = 2147483647 
i = 77 	int overflow	p = 2147483647 
i = 78 	int overflow	p = 2147483647 
i = 79 	int overflow	p = 2147483647 
i = 80 	int overflow	p = 2147483647 
i = 81 	int overflow	p = 2147483647 
i = 82 	int overflow	p = 2147483647 
i = 83 	int overflow	p = 2147483647 
i = 84 	int overflow	p = 2147483647 
i = 85 	int overflow	p = 2147483647 
i = 86 	int overflow	p = 2147483647 
i = 87 	int overflow	p = 2147483647 
i = 88 	int overflow	p = 2147483647 
i = 89 	int overflow	p = 2147483647 
i = 90 	int overflow	p = 2147483647 
i = 91 	int overflow	p = 2147483647 
i = 92 	int overflow	p = 2147483647 
i = 93 	int overflow	p = 2147483647 
i = 94 	int overflow	p = 2147483647 
i = 95 	int overflow	p = 2147483647 
i = 96 	int overflow	p = 2147483647 
i = 97 	int overflow	p = 2147483647 
i = 98 	int overflow	p = 2147483647 
i = 99 	int overflow	p = 2147483647 
i = 100 	int overflow	p = 2147483647 
product of integer numbers from 1 to 100 is 2147483647

Widać że każdy produkt powyżej górnego zakresu=12 jest błędny. Rozwiązaniem jest:

  • użycie bibliotek o dowolnej precyzji ( GMP )
  • użycie typu zmiennoprzecinkowego (double )
#include <stdio.h>

double double_product(const int m,  const int n )
{
   double p = 1.0;
   for(int i=m; i<=n; ++i)
   {
     p*=i;
     
   }
   return p;
}


int main()
{
   double p;
   int m = 1; // lower index
   int n = 100; // upper index
   
    p = double_product(m,n);
   
   printf("product of integer numbers from %d to %d is %.16e\n", m, n, p);
   
}
gcc d.c -Wall -Wextra
./a.out
product of integer numbers from 1 to 100 is 9.3326215443944102e+157

Jak widać pierwsze 17 cyfr znaczących się zgadza. Błąd wynosi około 10^140 czyli jest mniejszy niż 1%

zaawansowane algorytmy

[edytuj]
  • Jak tworzyć matematyczne oprogramowanie w C?[29]
  • drukowanie liczb algebraicznych[30]

Obliczenia numeryczne

[edytuj]

Obliczenia numeryczne[31] [32]są to obliczenia na liczbach. Ich przeciwieństwem są obliczenia symboliczne wykonywane na symbolach ( zobacz Maxima CAS ) [33]


Epsilon maszynowy

[edytuj]

Epsilon maszynowy jest wartością określającą precyzję obliczeń numerycznych wykonywanych na liczbach zmiennoprzecinkowych.[36]

Jest to najmniejsza liczba nieujemna, której dodanie do jedności daje wynik nierówny 1. Innymi słowy, jest to najmniejszy ε, dla którego następujący warunek jest uznawany za niespełniony (przyjmuje wartość fałsz): 1 + ε = 1

Im mniejsza wartość epsilona maszynowego, tym większa jest względna precyzja obliczeń.

Obliczmy epsilon dla liczb podwójnej precyzji :


/* 
http://en.wikipedia.org/wiki/Machine_epsilon
The following C program does not actually determine the machine epsilon;
rather, it determines a number within a factor of two (one order of magnitude) 
of the true machine epsilon, using a linear search.

---

The difference between 1 and the least value greater than 1 that is representable in the given floating-point type, b1-p.
-------------------------------
http://stackoverflow.com/questions/1566198/how-to-portably-get-dbl-epsilon-in-c-c

gcc m.c -Wall

./a.out
*/


#include <stdio.h>
#include <float.h> // DBL_EPSILON
 
 int main()
 {
    double epsilon = 1.0;
 
    printf( "epsilon;  1 + epsilon\n" );
    
    do 
     {
       printf( "%G\t%.20f\n", epsilon, (1.0 + epsilon) );
       epsilon /= 2.0f;
     }
    // If next epsilon yields 1, then break
    while ((1.0 + (epsilon/2.0)) != 1.0); // 

    // because current epsilon is the calculated machine epsilon.
    printf( "\nCalculated Machine epsilon: %G\n", epsilon );
    
    
    //check value from float.h , Steve Jessop
    if ((1.0 + DBL_EPSILON) != 1.0 
        && 
       (1.0 + DBL_EPSILON/2) == 1.0)
     printf("DBL_EPSILON = %g \n", DBL_EPSILON);
     else printf("DBL_EPSILON is not good !!! \n");


    return 0;
 }

Wynik programu :

epsilon;  1 + epsilon
1	2.00000000000000000000
0.5	1.50000000000000000000
0.25	1.25000000000000000000
0.125	1.12500000000000000000
0.0625	1.06250000000000000000
0.03125	1.03125000000000000000
0.015625	1.01562500000000000000
0.0078125	1.00781250000000000000
0.00390625	1.00390625000000000000
0.00195312	1.00195312500000000000
0.000976562	1.00097656250000000000
0.000488281	1.00048828125000000000
0.000244141	1.00024414062500000000
0.00012207	1.00012207031250000000
6.10352E-05	1.00006103515625000000
3.05176E-05	1.00003051757812500000
1.52588E-05	1.00001525878906250000
7.62939E-06	1.00000762939453125000
3.8147E-06	1.00000381469726562500
1.90735E-06	1.00000190734863281250
9.53674E-07	1.00000095367431640625
4.76837E-07	1.00000047683715820312
2.38419E-07	1.00000023841857910156
1.19209E-07	1.00000011920928955078
5.96046E-08	1.00000005960464477539
2.98023E-08	1.00000002980232238770
1.49012E-08	1.00000001490116119385
7.45058E-09	1.00000000745058059692
3.72529E-09	1.00000000372529029846
1.86265E-09	1.00000000186264514923
9.31323E-10	1.00000000093132257462
4.65661E-10	1.00000000046566128731
2.32831E-10	1.00000000023283064365
1.16415E-10	1.00000000011641532183
5.82077E-11	1.00000000005820766091
2.91038E-11	1.00000000002910383046
1.45519E-11	1.00000000001455191523
7.27596E-12	1.00000000000727595761
3.63798E-12	1.00000000000363797881
1.81899E-12	1.00000000000181898940
9.09495E-13	1.00000000000090949470
4.54747E-13	1.00000000000045474735
2.27374E-13	1.00000000000022737368
1.13687E-13	1.00000000000011368684
5.68434E-14	1.00000000000005684342
2.84217E-14	1.00000000000002842171
1.42109E-14	1.00000000000001421085
7.10543E-15	1.00000000000000710543
3.55271E-15	1.00000000000000355271
1.77636E-15	1.00000000000000177636
8.88178E-16	1.00000000000000088818
4.44089E-16	1.00000000000000044409

Calculated Machine epsilon: 2.22045E-16
DBL_EPSILON = 2.22045e-16 


Obliczmy epsilon dla liczb pojedynczej precyzji :

#include <stdio.h>
 
 int main()
 {
    float epsilon = 1.0f;
 
    printf( "epsilon;  1 + epsilon\n" );
    
    do 
     {
       printf( "%G\t%.20f\n", epsilon, (1.0f + epsilon) );
       epsilon /= 2.0f;
     }
    // If next epsilon yields 1, then break
    while ((float)(1.0 + (epsilon/2.0)) != 1.0); // 

    // because current epsilon is the machine epsilon.
    printf( "\nCalculated Machine epsilon: %G\n", epsilon );
    return 0;
 }


Wynik :

Calculated Machine epsilon: 1.19209E-07


Obliczmy epsilon dla liczb long double :

#include <stdio.h>
 
 int main()
 {
    long double epsilon = 1.0;
 
    printf( "epsilon;  1 + epsilon\n" );
    
    do 
     {
       printf( "%LG \t %.25LG \n", epsilon, (1.0 + epsilon) );
       epsilon /= 2.0;
     }
    // If next epsilon yields 1, then break
    while ((1.0 + (epsilon/2.0)) != 1.0); // 

    // because current epsilon is the machine epsilon.
    printf( "\n Calculated Machine epsilon: %LG\n", epsilon );
    return 0;
 }


Wynik :

 Calculated Machine epsilon: 1.0842E-19

Porównaj

wyjątki

[edytuj]

Wyjatki

  • programowe
  • sprzętowe

Przykłady

  • A floating-point exception (FPE) = Wyjątek zmiennoprzecinkowy nie jest wyjątkiem programowym. Powstaje na poziomie mikroprocesora lub ISA. FPE może spowodować wysłąnie sygnału o nazwie SIGFPE, z którym można sobie poradzić, ale nie za pomocą C[37]


Wyjątki zmiennoprzecinkowe są kontrolowane przez kod biblioteki w C99, a nie przez flagi kompilatora[38]


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

#define PRINTEXC(ex, val) printf(#ex ": %s\n", (val & ex) ? "set" : "unset");

double foo(double a, double b) { return sin(a) / b; }

int main()
{
    int e;
    double x;

    feclearexcept(FE_ALL_EXCEPT);

    x = foo(1.2, 3.1);

    e = fetestexcept(FE_ALL_EXCEPT);
    PRINTEXC(FE_DIVBYZERO, e);
    PRINTEXC(FE_INEXACT, e);
    PRINTEXC(FE_INVALID, e);
    PRINTEXC(FE_OVERFLOW, e);
    PRINTEXC(FE_UNDERFLOW, e);

    putchar('\n');

    feclearexcept(FE_ALL_EXCEPT);

    x += foo(1.2, 0.0);

    e = fetestexcept(FE_ALL_EXCEPT);
    PRINTEXC(FE_DIVBYZERO, e);
    PRINTEXC(FE_INEXACT, e);
    PRINTEXC(FE_INVALID, e);
    PRINTEXC(FE_OVERFLOW, e);
    PRINTEXC(FE_UNDERFLOW, e);
    return lrint(x);
}

Wynik:

    

FE_DIVBYZERO: unset
FE_INEXACT: set
FE_INVALID: unset
FE_OVERFLOW: unset
FE_UNDERFLOW: unset

FE_DIVBYZERO: set
FE_INEXACT: set
FE_INVALID: unset
FE_OVERFLOW: unset
FE_UNDERFLOW: unset


Jest też możliwe użycie sygnałów ( zobacz fenv.h):

    
#pragma STDC FENV_ACCESS on

#define _GNU_SOURCE
#include <fenv.h>

int main()
{
#ifdef FE_NOMASK_ENV
    fesetenv(FE_NOMASK_ENV);
#endif

    // ...
}

Limity dla obliczeń

[edytuj]

zmiennoprzecinkowych

[edytuj]

Definicje W pliku float.h są zdefiniowane stałe :[39]

  • DBL_MIN , czyli najmniejszą dodatnia liczbą typu double uznawaną przez maszynę za różną od zera [40]
  • DBL_MAX, czyli największa dodatnia liczbą typu double, która może być używana przez komputer

W pliku math.h są zdefiniowane :

// gcc -lm -Wall l.c
#include <stdio.h>
#include <math.h> // infinity, nan
#include <float.h>//DBL_MIN

int main(void)
{
  
  printf("DBL_MIN = %g \n", DBL_MIN);
  printf("DBL_MAX = %g \n", DBL_MAX);
  printf("INFINITY = %g \n",  INFINITY);
#ifdef NAN 
  printf("NAN= %g \n", NAN );
#endif
  return 0;
}


Wynik działania :

DBL_MIN = 2.22507e-308 
DBL_MAX = 1.79769e+308 
INFINITY = inf 
NAN= nan 


całkowitych

[edytuj]
/*

gcc l.c -lm -Wall
./a.out


http://stackoverflow.com/questions/29592898/do-long-long-and-long-have-same-range-in-c-in-64-bit-machine
*/
#include <stdio.h>
#include <math.h> // M_PI; needs -lm also 
#include <limits.h> // INT_MAX, http://pubs.opengroup.org/onlinepubs/009695399/basedefs/limits.h.html





int main(){

double lMax;


 lMax = log2(INT_MAX);
 printf("INT_MAX \t= %25d ; lMax = log2(INT_MAX) \t= %.0f \n",INT_MAX,  lMax); 

 lMax = log2(UINT_MAX);
 printf("UINT_MAX \t= %25u ; lMax = log2(UINT_MAX) \t= %.0f \n", UINT_MAX,  lMax); 

 lMax = log2(LONG_MAX);
 printf("LONG_MAX \t= %25ld ; lMax = log2(LONG_MAX) \t= %.0f \n",LONG_MAX,  lMax); 


 lMax = log2(ULONG_MAX);
 printf("ULONG_MAX \t= %25lu ; lMax = log2(ULONG_MAX) \t= %.0f \n",ULONG_MAX,  lMax); 

 lMax = log2(LLONG_MAX);
 printf("LLONG_MAX \t= %25lld ; lMax = log2(LLONG_MAX) \t= %.0f \n",LLONG_MAX, lMax); 

 lMax = log2(ULLONG_MAX);
 printf("ULLONG_MAX \t= %25llu ; lMax = log2(ULLONG_MAX) \t= %.0f \n",ULLONG_MAX, lMax); 





return 0;
}

Wynik :

INT_MAX 	=                2147483647 ; lMax = log2(INT_MAX) 	= 31 
UINT_MAX 	=                4294967295 ; lMax = log2(UINT_MAX) 	= 32 
LONG_MAX 	=       9223372036854775807 ; lMax = log2(LONG_MAX) 	= 63 
ULONG_MAX 	=      18446744073709551615 ; lMax = log2(ULONG_MAX) 	= 64 
LLONG_MAX 	=       9223372036854775807 ; lMax = log2(LLONG_MAX) 	= 63 
ULLONG_MAX 	=      18446744073709551615 ; lMax = log2(ULLONG_MAX) 	= 64 


Dla typów o stałej szerokości ( ang. Fixed width integer types ):

/*

gcc h.c -Wall -Wextra
./a.out

printf
------------------------------------ 
X ( specifier character): 
	input  = argument of type ‘unsigned int’
	output =  Unsigned hexadecimal integer (uppercase) without 0X prefix



# (  flags sub-specifier)	Used with X specifiers  : the value is preceeded with 0X respectively for values different than zero


*/
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h> // PRIu32 = all those nifty new format specifiers for the intN_t types and their brethren


int main(void)
{
	
	uint8_t  a = 0XFF; 		// 1 byte  =  8 bits
	uint16_t b = 0XFFFF;  		// 2 bytes = 16 bits
	uint32_t c = 0XFFFFFFFF;	// 4 bytes = 32 bits
	uint64_t d = 0XFFFFFFFFFFFFFFFF;// 8 bytes = 64 bits
	
	int bytes = 1;
	int bits = 8* bytes;
	
	
	printf("uint%d_t \ta ( %d byte  =  %d bits)\t = %#X (hexadecimal)\t\t\t= %d \t\t\t = 2^%d - 1  (decimal) \n", bits, bytes, bits, a, a, bits);
	bytes = 2; bits = 8 * bytes;
	printf("uint%d_t\tb ( %d byte  =  %d bits)\t = %#X (hexadecimal)\t\t\t= %d\t\t\t = 2^%d - 1  (decimal) \n", bits, bytes, bits, b, b, bits);
	bytes = 4; bits = 8 * bytes;
	printf("uint%d_t\tc ( %d byte  =  %d bits)\t = %#X (hexadecimal)\t\t= %"PRIu32" \t\t = 2^%d - 1  (decimal) \n", bits, bytes, bits, c, c, bits);
	bytes = 8; bits = 8 * bytes;
	printf("uint%d_t\tc ( %d byte  =  %d bits)\t = %#lX (hexadecimal)\t= %"PRIu64" \t = 2^%d - 1  (decimal) \n", bits, bytes, bits, d, d, bits);
	
	
	return 0;
}

Wynik:

uint8_t 	a ( 1 byte  =  8 bits)	 = 0XFF (hexadecimal)			= 255 			 = 2^8 - 1  (decimal) 
uint16_t	b ( 2 byte  =  16 bits)	 = 0XFFFF (hexadecimal)			= 65535			 = 2^16 - 1  (decimal) 
uint32_t	c ( 4 byte  =  32 bits)	 = 0XFFFFFFFF (hexadecimal)		= 4294967295 		 = 2^32 - 1  (decimal) 
uint64_t	c ( 8 byte  =  64 bits)	 = 0XFFFFFFFFFFFFFFFF (hexadecimal)	= 18446744073709551615 	 = 2^64 - 1  (decimal) 


Limity dla typów całkowitych o stałej szerokości
Typ Znak Bits Bytes Minimum Maximum
int8_t Signed 8 1 −27 = −128 27 − 1 = 127
uint8_t Unsigned 8 1 0 28 − 1 = 255
int16_t Signed 16 2 −215 = −32,768 215 − 1 = 32,767
uint16_t Unsigned 16 2 0 216 − 1 = 65,535
int32_t Signed 32 4 −231 = −2,147,483,648 231 − 1 = 2,147,483,647
uint32_t Unsigned 32 4 0 232 − 1 = 4,294,967,295
int64_t Signed 64 8 −263 = −9,223,372,036,854,775,808 263 − 1 = 9,223,372,036,854,775,807
uint64_t Unsigned 64 8 0 264 − 1 = 18,446,744,073,709,551,615
Przekroczenie zakresu liczb całkowitych
[edytuj]

Przekroczenie zakresu liczb całkowitych ( ang. integer overflow ) [41] może dotyczyć liczb całkowitych :[42]

  • bez znaku ( " Unsigned integers are defined to wrap around. " )
  • ze znakiem ( powoduje zachowanie niezdefiniowane - może to powodować Złe Rzeczy czyli zagrożenie bezpieczeństwa komputera [43] )
#include <stdio.h>

/* 

a signed integer overflow is undefined behaviour in C 
check b^i 

to compile : 
  gcc i.c -Wall
to run : 

  ./a.out

*/

int main() {

int i;
int b=2; // base 
int p=1; // p = b^i

for ( i=0 ; i<40; i++){
     
     printf(" b^i = %d ^ %d = %d \n", b, i, p);
     p *= b;
     
}


return 0; 
}


Program kompiluje się i uruchamia bez komunikatów o błędach ale wynik nie jest taki jak naiwnie moglibyśmy się spodziewać :


 b^i = 2 ^ 0 = 1 
 b^i = 2 ^ 1 = 2 
 b^i = 2 ^ 2 = 4 
 b^i = 2 ^ 3 = 8 
 b^i = 2 ^ 4 = 16 
 b^i = 2 ^ 5 = 32 
 b^i = 2 ^ 6 = 64 
 b^i = 2 ^ 7 = 128 
 b^i = 2 ^ 8 = 256 
 b^i = 2 ^ 9 = 512 
 b^i = 2 ^ 10 = 1024 
 b^i = 2 ^ 11 = 2048 
 b^i = 2 ^ 12 = 4096 
 b^i = 2 ^ 13 = 8192 
 b^i = 2 ^ 14 = 16384 
 b^i = 2 ^ 15 = 32768 
 b^i = 2 ^ 16 = 65536 
 b^i = 2 ^ 17 = 131072 
 b^i = 2 ^ 18 = 262144 
 b^i = 2 ^ 19 = 524288 
 b^i = 2 ^ 20 = 1048576 
 b^i = 2 ^ 21 = 2097152 
 b^i = 2 ^ 22 = 4194304 
 b^i = 2 ^ 23 = 8388608 
 b^i = 2 ^ 24 = 16777216 
 b^i = 2 ^ 25 = 33554432 
 b^i = 2 ^ 26 = 67108864 
 b^i = 2 ^ 27 = 134217728 
 b^i = 2 ^ 28 = 268435456 
 b^i = 2 ^ 29 = 536870912 
 b^i = 2 ^ 30 = 1073741824 
 b^i = 2 ^ 31 = -2147483648 
 b^i = 2 ^ 32 = 0 
 b^i = 2 ^ 33 = 0 
 b^i = 2 ^ 34 = 0 
 b^i = 2 ^ 35 = 0 
 b^i = 2 ^ 36 = 0 
 b^i = 2 ^ 37 = 0 
 b^i = 2 ^ 38 = 0 
 b^i = 2 ^ 39 = 0 

Na podstawie wyniku możemy ocenić że zmienna int jest typu 32 bitowego , ponieważ obliczenia do 2^30 są poprawne.

Dla liczb bez znaku przekroczenie zakresu powoduje inny efekt ( modulo ) :

#include <stdio.h>

/* 

 
 Unsigned integers are defined to wrap around.
 "When you work with unsigned types, modular arithmetic (also known as "wrap around" behavior) is taking place."
http://stackoverflow.com/questions/7221409/is-unsigned-integer-subtraction-defined-behavior

*/


int main() {

unsigned int i;
unsigned int old=0; // 
unsigned int new=0; // 
unsigned int p=1000000000; // 
//
unsigned long long int lnew= 0; //
unsigned long long int lold = (unsigned long long int) old; //
unsigned long long int lp = (unsigned long long int) p; //

printf("unsigned long long int \tunsigned  int \n"); // header 

for ( i=0 ; i<20; i++){
     printf("lnew = %12llu \tnew =  %12u",  lnew, new);
     // check overflow
     // http://stackoverflow.com/questions/2633661/how-to-check-integer-overflow-in-c/
     if ( new < old) printf("    unsigned integer overflow = wrap \n");
                     else printf("\n");          
                       
                        
     // unsigned int
     old=new; // save old value for comparison = overflow check 
     new = old + p ; // simple addition ; new value should be greater then old value 
     // unsigned long long int
     lold=lnew;
     lnew=lold+lp;      
}


return 0; 
}

Wynik :

unsigned long long int 	unsigned  int 
lnew =            0 	new =             0
lnew =   1000000000 	new =    1000000000
lnew =   2000000000 	new =    2000000000
lnew =   3000000000 	new =    3000000000
lnew =   4000000000 	new =    4000000000
lnew =   5000000000 	new =     705032704    unsigned integer overflow = wrap 
lnew =   6000000000 	new =    1705032704
lnew =   7000000000 	new =    2705032704
lnew =   8000000000 	new =    3705032704
lnew =   9000000000 	new =     410065408    unsigned integer overflow = wrap 
lnew =  10000000000 	new =    1410065408
lnew =  11000000000 	new =    2410065408
lnew =  12000000000 	new =    3410065408
lnew =  13000000000 	new =     115098112    unsigned integer overflow = wrap 
lnew =  14000000000 	new =    1115098112
lnew =  15000000000 	new =    2115098112
lnew =  16000000000 	new =    3115098112
lnew =  17000000000 	new =    4115098112
lnew =  18000000000 	new =     820130816    unsigned integer overflow = wrap 
lnew =  19000000000 	new =    1820130816




Zapobieganie
[edytuj]
  • sprawdzanie danych :[45]
    • przed wykonaniem działań [46][47]
    • po wykonaniu działań ( może być niebezpieczne dla liczb ze znakiem ponieważ niezdefiniowane zachowanie zagraża bezpieczeństwu komputera )
  • zwiększenie limitów poprzez :
    • zmianę typu ( int , long int, long long int )
    • użycie biblioteki o dowolnej precyzji ( np. GMP )


Zapobieganie: wykrywanie możliwego przepełnienia przed wykonaniem działania. Porównaj:

  • kod z scaler
  • kod z c-FAQ[49]

rozmiar

[edytuj]
/*
Here is a small C program 
that will print out the size in bytes 
of some basic C types on your machine. 


Paul Gribble | Summer 2012
This work is licensed under a Creative Commons Attribution 4.0 International License
http://gribblelab.org/CBootcamp/3_Basic_Types_Operators_And_Expressions.html

gcc b.c -Wall
./a.out
*/


#include <stdio.h>

int main(int argc, char *argv[]) {
        printf("a char is %ld bytes\n", sizeof(char));
        printf("an int is %ld bytes\n", sizeof(int));
        printf("an float is %ld bytes\n", sizeof(float));
        printf("a double is %ld bytes\n", sizeof(double));
        printf("a short int is %ld bytes\n", sizeof(short int));
        printf("a long int is %ld bytes\n", sizeof(long int));
        printf("a long double is %ld bytes\n", sizeof(long double));
        return 0;
}



a char is 1 bytes
an int is 4 bytes
an float is 4 bytes
a double is 8 bytes
a short int is 2 bytes
a long int is 8 bytes
a long double is 16 bytes


Liczba cyfr

[edytuj]

Liczba cyfr w liczbie zmiennoprzecinkowej [50]

// http://ubuntuforums.org/showthread.php?t=986212
// http://www.cplusplus.com/reference/cfloat/
// gcc d.c -lm -Wall
// ./a.out

#include <stdio.h>
#include <float.h> 

int main(void)
{
    printf("Float can ensure %d decimal places\n", FLT_DIG);
    printf("Double can ensure %d decimal places\n", DBL_DIG);
    printf("Long double can ensure %d decimal places\n", LDBL_DIG);

    return 0;
}

Wynik :

Float can ensure 6 decimal places
Double can ensure 15 decimal places
Long double can ensure 18 decimal places

Liczby subnormalne

[edytuj]
przybliżenia DBL_MIN i liczby subnormalnej
[edytuj]

Korzystając z funkcji isnormal zdefiniowanej w pliku math.h możemy samodzielnie poszukać przybliżenia DBL_MIN i liczby subnormalnej.

/* 
isnormal  example 
ISO C99
http://www.cplusplus.com/reference/cmath/isnormal/
http://www.gnu.org/software/libc/manual/html_node/Floating-Point-Classes.html
http://docs.oracle.com/cd/E19957-01/806-3568/ncg_math.html

compile with: 
gcc -std=c99 s.c -lm

run :
./a.out

*/

#include <stdio.h> /* printf */
#include <math.h> /* isnormal */

int TestNumber(double x)
{
  int f; // flag

  f= isnormal(x);
  if (f) 
    printf (" = %g ; number is normal \n",x);
    else printf (" = %g ; number is not normal = denormal  \n",x);

  return f;
}


int main()
{

  double d ;
  double MinNormal; 
  int flag;
  
  d = 1.0 ; // normal
  flag = TestNumber(d);
  do 
  { 
   MinNormal=d;
   d /=2.0; // look for subnormal 
   flag = TestNumber(d);
   
   }
  while (flag);

  printf ("number %f = %g = %e is a approximation of minimal positive double normal \n",MinNormal, MinNormal, MinNormal);
  printf ("number %f = %g = %e is not normal ( subnormal) \n",d, d , d);
   
 return 0;
}

Wynik działania :

number 0.000000 = 2.22507e-308 = 2.225074e-308 is a approximation of minimal positive double normal 
number 0.000000 = 1.11254e-308 = 1.112537e-308 is not normal ( subnormal) 


eliminacja liczb subnormalnych
[edytuj]

Ten program generuje liczby subnormale:

/*
https://blogs.oracle.com/d/subnormal-numbers
gcc -O0 f.c 
*/
#include <stdio.h>
void main()
{
  double d=1.0;
  while (d>0) {printf("%e\\n",d); d=d/2.0;}
}


wynik:

1.000000e+00
5.000000e-01
2.500000e-01
1.250000e-01
6.250000e-02
3.125000e-02
...
3.162020e-322
1.581010e-322
7.905050e-323
3.952525e-323
1.976263e-323
9.881313e-324
4.940656e-324

Jeśli jednak skompilujemy go z opcję:

  gcc -O0 -ffast-math f.c

to otrzymamy:

...
3.560118e-307
1.780059e-307
8.900295e-308
4.450148e-308
2.225074e-308

Liczby subnormalne są zaokrąglane do zera.

Jaki wpływ na obliczenia mają liczby subnormalne?
[edytuj]
  • wydłużają czas obliczeń[51]

część ułamkowa

[edytuj]

Za pomocą:[52]

  • funkcji modf
  • konwersji int
double frac = r - (int)r ;

Błędy w obliczeniach numerycznych

[edytuj]

Typy:[53][54]

  • wg etapu operacji :[55]
    • blędne dane wejściowe : niezgodne z oczekiwanym typem
    • dane wejściowe powodują błąd rezultatu
  • wg rodzaju operacji


Efekt:

  • zachowanie niezdefiniowane : ( ang. undefined behavior = UB)[66] [67]

Dzielenie przez zero

[edytuj]

Dzielenie przez zero[68]

Kiedy dzielnik ma wartość zero w wyrażeniu (zwykle przez pomyłkę) to następuje awaria programu (nieprawidłowe zakończenie an. crash). Nieprawidłowe zakończenie może być poważnym problemem w przypadku oprogramowania o krytycznym znaczeniu dla życia.

Zapobiegać temu można przez:

  • kontrole if-else
  • obsługę wyjątków


Mnożenie

[edytuj]
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/* 
https://math.stackexchange.com/questions/2453939/is-this-characteristic-of-tent-map-usually-observed
*/

/* ------------ constans  ---------------------------- */
double m = 2.0; /* parameter of tent map */
double a = 1.0; /* upper bound for randum number generator */
int iMax = 100;
/* ------------------- functions --------------------------- */

/* 
tent map 
https://en.wikipedia.org/wiki/Tent_map
*/
double f(double x0, double m){

	double x1;
	if (x0 < 0.5) 
		x1 = m*x0;
		else x1 = m*(1.0 - x0);
	return x1;

}



/* random double from 0.0 to a 
https://stackoverflow.com/questions/13408990/how-to-generate-random-float-number-in-c


*/
double GiveRandom(double a){
	srand((unsigned int)time(NULL));
	
	return  (((double)rand()/(double)(RAND_MAX)) * a);

}

int main(void){

	int i = 0;
	double x = GiveRandom(a); /* x0 = random */
	
	for (i = 0; i<iMax; i++){
	
		printf("i = %3d \t x = %.16f\n",i, x);
		x = f(x,m); /* iteration of the tent map */
	}

	return 0;
}

Kompilacja i uruchomienie:

  gcc t.c -Wall
  ./a.out

Wynik:



i =   0 	 x = 0.1720333817284710
i =   1 	 x = 0.3440667634569419
i =   2 	 x = 0.6881335269138839
i =   3 	 x = 0.6237329461722323
i =   4 	 x = 0.7525341076555354
i =   5 	 x = 0.4949317846889292
i =   6 	 x = 0.9898635693778584
i =   7 	 x = 0.0202728612442833
i =   8 	 x = 0.0405457224885666
i =   9 	 x = 0.0810914449771332
i =  10 	 x = 0.1621828899542663
i =  11 	 x = 0.3243657799085327
i =  12 	 x = 0.6487315598170653
i =  13 	 x = 0.7025368803658694
i =  14 	 x = 0.5949262392682613
i =  15 	 x = 0.8101475214634775
i =  16 	 x = 0.3797049570730451
i =  17 	 x = 0.7594099141460902
i =  18 	 x = 0.4811801717078197
i =  19 	 x = 0.9623603434156394
i =  20 	 x = 0.0752793131687213
i =  21 	 x = 0.1505586263374425
i =  22 	 x = 0.3011172526748851
i =  23 	 x = 0.6022345053497702
i =  24 	 x = 0.7955309893004596
i =  25 	 x = 0.4089380213990808
i =  26 	 x = 0.8178760427981615
i =  27 	 x = 0.3642479144036770
i =  28 	 x = 0.7284958288073540
i =  29 	 x = 0.5430083423852921
i =  30 	 x = 0.9139833152294159
i =  31 	 x = 0.1720333695411682
i =  32 	 x = 0.3440667390823364
i =  33 	 x = 0.6881334781646729
i =  34 	 x = 0.6237330436706543
i =  35 	 x = 0.7525339126586914
i =  36 	 x = 0.4949321746826172
i =  37 	 x = 0.9898643493652344
i =  38 	 x = 0.0202713012695312
i =  39 	 x = 0.0405426025390625
i =  40 	 x = 0.0810852050781250
i =  41 	 x = 0.1621704101562500
i =  42 	 x = 0.3243408203125000
i =  43 	 x = 0.6486816406250000
i =  44 	 x = 0.7026367187500000
i =  45 	 x = 0.5947265625000000
i =  46 	 x = 0.8105468750000000
i =  47 	 x = 0.3789062500000000
i =  48 	 x = 0.7578125000000000
i =  49 	 x = 0.4843750000000000
i =  50 	 x = 0.9687500000000000
i =  51 	 x = 0.0625000000000000
i =  52 	 x = 0.1250000000000000
i =  53 	 x = 0.2500000000000000
i =  54 	 x = 0.5000000000000000
i =  55 	 x = 1.0000000000000000
i =  56 	 x = 0.0000000000000000
i =  57 	 x = 0.0000000000000000
i =  58 	 x = 0.0000000000000000
i =  59 	 x = 0.0000000000000000
i =  60 	 x = 0.0000000000000000
i =  61 	 x = 0.0000000000000000
i =  62 	 x = 0.0000000000000000
i =  63 	 x = 0.0000000000000000
i =  64 	 x = 0.0000000000000000
i =  65 	 x = 0.0000000000000000
i =  66 	 x = 0.0000000000000000
i =  67 	 x = 0.0000000000000000
i =  68 	 x = 0.0000000000000000
i =  69 	 x = 0.0000000000000000
i =  70 	 x = 0.0000000000000000
i =  71 	 x = 0.0000000000000000
i =  72 	 x = 0.0000000000000000
i =  73 	 x = 0.0000000000000000
i =  74 	 x = 0.0000000000000000
i =  75 	 x = 0.0000000000000000
i =  76 	 x = 0.0000000000000000
i =  77 	 x = 0.0000000000000000
i =  78 	 x = 0.0000000000000000
i =  79 	 x = 0.0000000000000000
i =  80 	 x = 0.0000000000000000
i =  81 	 x = 0.0000000000000000
i =  82 	 x = 0.0000000000000000
i =  83 	 x = 0.0000000000000000
i =  84 	 x = 0.0000000000000000
i =  85 	 x = 0.0000000000000000
i =  86 	 x = 0.0000000000000000
i =  87 	 x = 0.0000000000000000
i =  88 	 x = 0.0000000000000000
i =  89 	 x = 0.0000000000000000
i =  90 	 x = 0.0000000000000000
i =  91 	 x = 0.0000000000000000
i =  92 	 x = 0.0000000000000000
i =  93 	 x = 0.0000000000000000
i =  94 	 x = 0.0000000000000000
i =  95 	 x = 0.0000000000000000
i =  96 	 x = 0.0000000000000000
i =  97 	 x = 0.0000000000000000
i =  98 	 x = 0.0000000000000000
i =  99 	 x = 0.0000000000000000

Porównywanie

[edytuj]

Sprawdźmy czy liczba x jest równa zero :

if (x==0.0)


Czy takie porównanie jest bezpieczne dla liczb zmiennoprzecinkowych ?[69]


//  gcc c1.c -Wall -lm
#include <math.h> /* isnormal */
#include <float.h>//DBL_MIN
#include <stdio.h>

int main ()
{
  double x = 1.0;
 

  
 
  int i;
  for ( i=0; i < 334; i++)
    {
      x/=10;
      printf ("i = %3d ; x= %.16lf = %e so  ", i, x,x);
      //
      if (x<DBL_MIN) printf ("x < DBL_MIN and ");
          else printf ("x > DBL_MIN  and  ");
      //
      if (isnormal(x)) printf ("x is normal and ");
          else printf ("x is  subnormal and  ");
      //
      if (x==0.0) printf ("equal to 0.0\n");
      else printf ("not equal to 0.0\n");
    }

return 0;
}

Wynik :


i =   0 ; x= 0.1000000000000000 = 1.000000e-01 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   1 ; x= 0.0100000000000000 = 1.000000e-02 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   2 ; x= 0.0010000000000000 = 1.000000e-03 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   3 ; x= 0.0001000000000000 = 1.000000e-04 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   4 ; x= 0.0000100000000000 = 1.000000e-05 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   5 ; x= 0.0000010000000000 = 1.000000e-06 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   6 ; x= 0.0000001000000000 = 1.000000e-07 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   7 ; x= 0.0000000100000000 = 1.000000e-08 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   8 ; x= 0.0000000010000000 = 1.000000e-09 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =   9 ; x= 0.0000000001000000 = 1.000000e-10 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  10 ; x= 0.0000000000100000 = 1.000000e-11 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  11 ; x= 0.0000000000010000 = 1.000000e-12 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  12 ; x= 0.0000000000001000 = 1.000000e-13 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  13 ; x= 0.0000000000000100 = 1.000000e-14 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  14 ; x= 0.0000000000000010 = 1.000000e-15 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  15 ; x= 0.0000000000000001 = 1.000000e-16 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  16 ; x= 0.0000000000000000 = 1.000000e-17 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  17 ; x= 0.0000000000000000 = 1.000000e-18 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  18 ; x= 0.0000000000000000 = 1.000000e-19 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  19 ; x= 0.0000000000000000 = 1.000000e-20 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i =  20 ; x= 0.0000000000000000 = 1.000000e-21 so  x > DBL_MIN  and  x is normal and not equal to 0.0
...
i = 290 ; x= 0.0000000000000000 = 1.000000e-291 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 291 ; x= 0.0000000000000000 = 1.000000e-292 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 292 ; x= 0.0000000000000000 = 1.000000e-293 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 293 ; x= 0.0000000000000000 = 1.000000e-294 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 294 ; x= 0.0000000000000000 = 1.000000e-295 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 295 ; x= 0.0000000000000000 = 1.000000e-296 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 296 ; x= 0.0000000000000000 = 1.000000e-297 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 297 ; x= 0.0000000000000000 = 1.000000e-298 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 298 ; x= 0.0000000000000000 = 1.000000e-299 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 299 ; x= 0.0000000000000000 = 1.000000e-300 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 300 ; x= 0.0000000000000000 = 1.000000e-301 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 301 ; x= 0.0000000000000000 = 1.000000e-302 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 302 ; x= 0.0000000000000000 = 1.000000e-303 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 303 ; x= 0.0000000000000000 = 1.000000e-304 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 304 ; x= 0.0000000000000000 = 1.000000e-305 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 305 ; x= 0.0000000000000000 = 1.000000e-306 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 306 ; x= 0.0000000000000000 = 1.000000e-307 so  x > DBL_MIN  and  x is normal and not equal to 0.0
i = 307 ; x= 0.0000000000000000 = 1.000000e-308 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 308 ; x= 0.0000000000000000 = 1.000000e-309 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 309 ; x= 0.0000000000000000 = 1.000000e-310 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 310 ; x= 0.0000000000000000 = 1.000000e-311 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 311 ; x= 0.0000000000000000 = 1.000000e-312 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 312 ; x= 0.0000000000000000 = 1.000000e-313 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 313 ; x= 0.0000000000000000 = 1.000000e-314 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 314 ; x= 0.0000000000000000 = 1.000000e-315 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 315 ; x= 0.0000000000000000 = 1.000000e-316 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 316 ; x= 0.0000000000000000 = 9.999997e-318 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 317 ; x= 0.0000000000000000 = 9.999987e-319 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 318 ; x= 0.0000000000000000 = 9.999889e-320 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 319 ; x= 0.0000000000000000 = 9.999889e-321 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 320 ; x= 0.0000000000000000 = 9.980126e-322 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 321 ; x= 0.0000000000000000 = 9.881313e-323 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 322 ; x= 0.0000000000000000 = 9.881313e-324 so  x < DBL_MIN and x is  subnormal and  not equal to 0.0
i = 323 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 324 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 325 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 326 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 327 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 328 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 329 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 330 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 331 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 332 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0
i = 333 ; x= 0.0000000000000000 = 0.000000e+00 so  x < DBL_MIN and x is  subnormal and  equal to 0.0


Jak powinno się porównywać liczby zmienno przecinkowe ?[70]

  • wartość bezwględna róznicy : if( abs(a-b) < epsilon) // wrong - don't do this[71]
  • if( abs((a-b)/b) < epsilon ) // still not right!
  • wartości graniczne
  • stałe [72]
 if (fpclassify(x) == FP_ZERO )

lub

 if (x == FP_ZERO)

Wartości służące do testo wania porównań :

  • wg Michael Borgwardt[73]

Sumowanie

[edytuj]

Na ile poważny jest to problem? Spróbujmy przyjrzeć się działaniu, polegającym na 1000-krotnym dodawaniu do liczby wartości 1/3. Oto kod:

#include <stdio.h>

int main ()
{
  float a = 0;
  int i = 0;
  for (;i<1000;i++)
  {
    a += 1.0/3.0;
  }
  printf ("%f\n", a);
}

Z matematyki wynika, że 1000*(1/3) = 333.(3), podczas gdy komputer wypisze wynik, nieco różniący się od oczekiwanego (w moim przypadku):

333.334106

Błąd pojawił się na cyfrze części tysięcznej liczby. Nie jest to może poważny błąd, jednak zastanówmy się, czy ten błąd nie będzie się powiększał. Zamieniamy w kodzie ilość iteracji z 1000 na 100 000. Tym razem mój komputer wskazał już nieco inny wynik:

33356.554688

Błąd przesunął się na cyfrę dziesiątek w liczbie. Tak więc nie należy do końca polegać na prezentacji liczb zmiennoprzecinkowych w komputerze.

Utrata precyzji

[edytuj]

Utrata precyzji, utrata cyfr znaczących ( ang. Loss of significance, catastrophic cancellation of significant digits)

  • sumowanie dużej liczby z małą
  • odejmowanie prawie równych liczb[74]

Biblioteki matematyczne

[edytuj]
Dependency Graph for tgmath.h

float.h

[edytuj]

opis

math.h

[edytuj]

Aby móc korzystać z wszystkich dobrodziejstw funkcji matematycznych musimy na początku dołączyć plik math.h:

 #include <math.h>

a w procesie kompilacji (dotyczy kompilatora GCC) musimy dodać flagę "-lm" po nazwie pliku wynikowego[75], czyli na końcu linii :[76]

Flaga lm jest zależna od środowiska. Na przykład, w systemie Windows, nie jest to wymagana, ale jest to wymagana w systemach opartych na systemie UNIX.

gcc plik.c -o plik -lm

Funkcje matematyczne, które znajdują się w bibliotece standardowej ( plik libm.a ) możesz znaleźć tutaj. Przy korzystaniu z nich musisz wziąć pod uwagę m.in. to, że biblioteka matematyczna prowadzi kalkulację w oparciu o radiany a nie stopnie.


Wersje

Stałe matematyczne: pi, e ...

[edytuj]

W pliku math.h zdefiniowane są pewne stałe, które mogą być przydatne do obliczeń. Są to m.in.:

  • M_E - podstawa logarytmu naturalnego (e, liczba Eulera)
  • M_LOG2E - logarytm o podstawie 2 z liczby e
  • M_LOG10E - logarytm o podstawie 10 z liczby e
  • M_LN2 - logarytm naturalny z liczby 2
  • M_LN10 - logarytm naturalny z liczby 10
  • M_PI - liczba π
  • M_PI_2 - liczba π/2
  • M_PI_4 - liczba π/4
  • M_1_PI - liczba 1/π
  • M_2_PI - liczba 2/π


Możemy to sprawdzić:

grep -i pi /usr/include/math.h

i otrzymamy:

# define M_PI        3.14159265358979323846    /* pi */
# define M_PI_2        1.57079632679489661923    /* pi/2 */
# define M_PI_4        0.78539816339744830962    /* pi/4 */
# define M_1_PI        0.31830988618379067154    /* 1/pi */
# define M_2_PI        0.63661977236758134308    /* 2/pi */
# define M_2_SQRTPI    1.12837916709551257390    /* 2/sqrt(pi) */
# define M_PIl        3.1415926535897932384626433832795029L  /* pi */
# define M_PI_2l    1.5707963267948966192313216916397514L  /* pi/2 */
# define M_PI_4l    0.7853981633974483096156608458198757L  /* pi/4 */
# define M_1_PIl    0.3183098861837906715377675267450287L  /* 1/pi */
# define M_2_PIl    0.6366197723675813430755350534900574L  /* 2/pi */
# define M_2_SQRTPIl    1.1283791670955125738961589031215452L  /* 2/sqrt(pi) */
/* When compiling in strict ISO C compatible mode we must not use the

Liczby zespolone

[edytuj]


Typy

  • _Complex
  • complex.h
  • biblioteki:


różnica pomiędzy _complex a complex

[edytuj]
  • _Complex jest słowem kluczowym, możemy go używać bez dyrektywy include dołączającej plik nagłówkowy complex.h
  • complex jest makrem z pliku nagłówkowego complex.h
float _Complex     
double _Complex                  
long double _Complex


 #include <complex.h>
 float complex
double complex
long double complex

complex.h

[edytuj]

Operacje na liczbach zespolonych są częścią uaktualnionego standardu języka C o nazwie C99, który jest obsługiwany jedynie przez część kompilatorów

Dotychczas korzystaliśmy tylko z liczb rzeczywistych, lecz najnowsze standardy języka C umożliwiają korzystanie także z innych liczb - np. z liczb zespolonych.

Aby móc korzystać z liczb zespolonych w naszym programie należy w nagłówku programu umieścić następującą linijkę:

 #include <complex.h>

która powoduje dołączenie standardowej biblioteki obsługującej liczny zespolenie

Wiemy, że liczba zespolona zdeklarowana jest następująco:

z = a+b*i,

gdzie a, b są liczbami rzeczywistymi, a i jest jednostką urojoną

i*i = (-1).

W pliku complex.h liczba i zdefiniowana jest jako I. Zatem wypróbujmy możliwości liczb zespolonych:

 #include <math.h>
 #include <complex.h>
 #include <stdio.h>
 
 int main ()
 {
   float _Complex z = 4+2.5*I;
   printf ("Liczba z: %f+%fi\n", creal(z), cimag (z));
   return 0;
 }

następnie kompilujemy nasz program:

gcc plik1.c -o plik1 -lm

Po wykonaniu naszego programu powinniśmy otrzymać:

Liczba z: 4.00+2.50i

W programie zamieszczonym powyżej użyliśmy dwóch funkcji - creal i cimag.

  • creal - zwraca część rzeczywistą liczby zespolonej
  • cimag - zwraca część urojoną liczby zespolonej

Więcej:

// https://stackoverflow.com/questions/6418807/how-to-work-with-complex-numbers-in-c
// program by user870774
#include <stdio.h>      /* Standard Library of Input and Output */
#include <complex.h>    /* Standard Library of Complex Numbers */

int main() {

    double complex z1 = 1.0 + 3.0 * I;
    double complex z2 = 1.0 - 4.0 * I;

    printf("Working with complex numbers:\n\v");

    printf("Starting values: Z1 = %.2f + %.2fi\tZ2 = %.2f %+.2fi\n", creal(z1), cimag(z1), creal(z2), cimag(z2));

    double complex sum = z1 + z2;
    printf("The sum: Z1 + Z2 = %.2f %+.2fi\n", creal(sum), cimag(sum));

    double complex difference = z1 - z2;
    printf("The difference: Z1 - Z2 = %.2f %+.2fi\n", creal(difference), cimag(difference));

    double complex product = z1 * z2;
    printf("The product: Z1 x Z2 = %.2f %+.2fi\n", creal(product), cimag(product));

    double complex quotient = z1 / z2;
    printf("The quotient: Z1 / Z2 = %.2f %+.2fi\n", creal(quotient), cimag(quotient));

    double complex conjugate = conj(z1);
    printf("The conjugate of Z1 = %.2f %+.2fi\n", creal(conjugate), cimag(conjugate));

    return 0;
}

MPC

[edytuj]

Opis MPC

Dodatkowe

[edytuj]

Zobacz również

[edytuj]

Źródła

[edytuj]
  1. http://tldp.org/HOWTO/Program-Library-HOWTO/
  2. stackoverflow question: linux-error-while-loading-shared-libraries-cannot-open-shared-object-file-no-s
  3. Programowanie funkcyjne: Modu ze strony wazniak.mimuw.edu.pl
  4. Programowanie funkcyjne: Modu ze strony wazniak.mimuw.edu.pl
  5. stackoverflow question : difference-between-static-and-shared-libraries
  6. Static, Shared Dynamic and Loadable Linux Libraries from yolinux
  7. HOWTO : shared-libraries
  8. drepper : how to write shared library
  9. Linux Libraries (Paths, Files, and Commands) by DevynCJohnson
  10. gcc : Search-Path
  11. Where Does GCC Look to Find its Header Files? by Joshua Davie
  12. stackoverflow :How to add a default include path for gcc in linux?
  13. Stackoverflow : GCC include directories
  14. analyzing-c-source-files-dependencies-in-a-program by balau82
  15. make file tutorial by Chase Lambert and contrib.
  16. The Rational Number Class in C by Peter Burden
  17. Rational Arithmetic by R. Sedgewick
  18. Where is the itoa function in Linux?
  19. gcc - Binary-constants
  20. Code Replay : C represent int in base 2
  21. what-every-computer-programmer-should by Josh Haberman
  22. Metody numeryczne - autorzy : Piotr Krzyżanowski i Leszek Plaskota — Uniwersytet Warszawski, Wydział Matematyki, Informatyki i Mechaniki
  23. How to get memory representation-double
  24. IEEE-754 Floating Point Converter
  25. dumpfp: A Tool to Inspect Floating-Point Numbers by Joshua Haberman
  26. subethasoftware : converting-two-8-bit-values-to-one-16-bit-value-in-c
  27. subethasoftware: splitting-a-16-bit-value-to-two-8-bit-values-in-c
  28. Math to Code by cameron smith
  29. developing-mathematical-software-in-c by Fredrik Johansson
  30. printing-algebraic-numbers by Fredrik Johansson
  31. numeryczne - Wydziału MIM UW
  32. Cpp Core Guidelines : arithmetic
  33. i ból obliczeń numerycznych -Piotr Krzyżanowski
  34. Every Computer Scientist Should Know About Floating-Point Arithmetic, by David Goldberg
  35. Two disasters caused by computer arithmetic errors
  36. Praktyczne wyznaczanie precyzji arytmetyki - autorzy : Piotr Krzyżanowski i Leszek Plaskota — Uniwersytet Warszawski, Wydział Matematyki, Informatyki i Mechaniki
  37. stackoverflow question : why-does-gcc-report-a-floating-point-exception-when-i-execute-1-0
  38. stackoverflow question: catch-floating-point-exceptions-using-a-compiler-option-with-c
  39. Point Representation - Basics from : geeksforgeeks.org
  40. Tutorial on Data Representation by Chua Hock-Chuan
  41. Przekroczenie zakresu liczb całkowitych w wikipedii
  42. [http://www.fefe.de/intof.html%7CCatching Integer Overflows in C ]
  43. Guide to Undefined Behavior in C and C++, Part 1 by John Regehr
  44. stackoverflow question: function-abs-returning-negative-number-in-c
  45. Testing for Integer Overflow in C and C++ by Josh Haberman
  46. comp.lang.c FAQ list · Question 20.6b : How can I ensure that integer arithmetic doesn't overflow ?
  47. GCC : Built-in Functions to Perform Arithmetic with Overflow Checking
  48. gnulib : Checking-Integer-Overflow
  49. c-faq: three functions for ``careful addition, subtraction, and multiplication
  50. Stackoverflow : Counting digits in a float
  51. O N SUBNORMAL FLOATING POINT AND ABNORMAL TIMING by Marc Andrysco, David Kohlbrenner, Keaton Mowery, Ranjit Jhala, Sorin Lerner, and Hovav Shacham
  52. stackoverflow question : extract-decimal-part-from-a-floating-point-number-in-c
  53. INTRODUCTION TO NUMERICAL ANALYSIS WITH C PROGRAMS by Attila Mate
  54. Basic Issues in Floating Point Arithmetic and Error Analysis by Jim Demmel
  55. securecoding.cert.org : Prevent+or+detect+domain+and+range+errors+in+math+functions
  56. Floating point inaccuracy examples
  57. Catastrophic Cancellation: The Pitfalls of Floating Point Arithmetic by Graham Markall
  58. math.stackexchange question: is-this-characteristic-of-tent-map-usually-observed
  59. BŁĘDY PRZETWARZANIA NUMERYCZNEGO, Maciej Patan, UW Zielonogórski
  60. dangerous is it to compare floating point values?
  61. it possible to get 0 by subtracting two unequal floating point numbers?
  62. How to solve quadratic equations numerically by FLORIAN DANG
  63. Double Rounding Errors in Floating-Point Conversions By Rick Regan (Published August 11th, 2010)
  64. stackoverflow question when-does-underflow-occur
  65. stackoverflow question: how-to-define-underflow-for-an-implementationieee754-which-support-subnormal-n
  66. Undefined_behavior w ang. wikipedii
  67. undefined-behavior-in-c-and-cplusplus-programs by Nayuki
  68. delftstack :infinity-in-c by Abdul Mateen Oct-26, 2022
  69. stackoverflow question: c-floating-point-zero-comparison
  70. Comparing Floating-Point Numbers Is Tricky by Matt Kline
  71. | floating-point-gui.de : comparison/
  72. stackoverflow question : float-double-equality-with-exact-zero
  73. Michael Borgwardt : Nearly Equals Test in java
  74. numerically-stable-law-of-cosines by Nayuki
  75. Stackoverflow : Undefined reference to `pow' and `floor'
  76. I'm using math.h and the library link -lm, but “undefined reference to `pow'” still happening
  77. arb library



Powszechne praktyki

[edytuj]

Rozdział ten ma za zadanie pokazać powszechnie stosowane metody programowania w C. Nie będziemy tu uczyć, jak należy stawiać nawiasy klamrowe ani który sposób nazewnictwa zmiennych jest najlepszy - prowadzone są o to spory, z których niewiele wynika. Zaprezentowane tu rozwiązania mają konkretny wpływ na jakość tworzonych programów.


Konstruktory i destruktory

[edytuj]

W większości obiektowych języków programowania obiekty nie mogą być tworzone bezpośrednio - obiekty otrzymuje się wywołując specjalną metodę danej klasy, zwaną konstruktorem. Konstruktory są ważne, ponieważ pozwalają zapewnić obiektowi odpowiedni stan początkowy. Destruktory, wywoływane na końcu czasu życia obiektu, są istotne, gdy obiekt ma wyłączny dostęp do pewnych zasobów i konieczne jest upewnienie się, czy te zasoby zostaną zwolnione.

Ponieważ C nie jest językiem obiektowym, nie ma wbudowanego wsparcia dla konstruktorów i destruktorów. Często programiści bezpośrednio modyfikują tworzone obiekty i struktury. Jednakże prowadzi to do potencjalnych błędów, ponieważ operacje na obiekcie mogą się nie powieść lub zachować się nieprzewidywalnie, jeśli obiekt nie został prawidłowo zainicjalizowany. Lepszym podejściem jest stworzenie funkcji, która tworzy instancję obiektu, ewentualnie przyjmując pewne parametry:

 struct string {
   size_t size;
   char *data;
 };
 
 struct string *create_string(const char *initial) {
   assert (initial != NULL);
   struct string *new_string = malloc(sizeof(*new_string));
   if (new_string != NULL) {
     new_string->size = strlen(initial);
     new_string->data = strdup(initial);
   }
   return new_string;
 }

Podobnie, bezpośrednie usuwanie obiektów może nie do końca się udać, prowadząc do wycieku zasobów. Lepiej jest użyć destruktora:

 void free_string(struct string *s) 
 {
   assert (s != NULL);
   free(s->data);  /* zwalniamy pamięć zajmowaną przez strukturę */
   free(s);        /* usuwamy samą strukturę */
 }

Często łączy się destruktory z zerowaniem zwolnionych wskaźników.

Czasami dobrze jest ukryć definicję obiektu, żeby mieć pewność, że użytkownicy nie utworzą go ręcznie. Aby to zapewnić struktura jest definiowana w pliku źródłowym (lub prywatnym nagłówku niedostępnym dla użytkowników) zamiast w pliku nagłówkowym, a deklaracja wyprzedzająca jest umieszczona w pliku nagłówkowym:

 struct string;
 struct string *create_string(const char *initial);
 void free_string(struct string *s);

Zerowanie zwolnionych wskaźników

[edytuj]

Jak powiedziano już wcześniej, po wywołaniu free() dla wskaźnika, staje się on "wiszącym wskaźnikiem". Co gorsze, większość nowoczesnych platform nie potrafi wykryć, kiedy taki wskaźnik jest używany zanim zostanie ponownie przypisany.

Jednym z prostych rozwiązań tego problemu jest zapewnienie, że każdy wskaźnik jest zerowany natychmiast po zwolnieniu:

 free(p);
 p = NULL;

Inaczej niż w przypadku "wiszących wskaźników", na wielu nowoczesnych architekturach przy próbie użycia wyzerowanego wskaźnika pojawi się sprzętowy wyjątek. Dodatkowo, programy mogą zawierać sprawdzanie błędów dla zerowych wartości, ale nie dla "wiszących wskaźników". Aby zapewnić, że jest to wykonywane dla każdego wskaźnika, możemy użyć makra:

 #define FREE(p)   do { free(p); (p) = NULL; } while(0)

(aby zobaczyć dlaczego makro jest napisane w ten sposób, zobacz #Konwencje pisania makr)

Przy wykorzystaniu tej techniki destruktory powinny zerować wskaźnik, który przekazuje się do nich, więc argument musi być do nich przekazywany przez referencję. Na przykład, oto zaktualizowany destruktor z sekcji Konstruktory i destruktory:

 void free_string(struct string **s) 
 {
   assert(s != NULL  &&  *s != NULL);
   FREE((*s)->data);  /* zwalniamy pamięć zajmowaną przez strukturę */
   FREE(*s);          /* usuwamy strukturę */
 }

Niestety, ten idiom nie jest wstanie pomóc w wypadku wskazywania przez inne wskaźniki zwolnionej pamięci. Z tego powodu niektórzy eksperci C uważają go za niebezpieczny, jako kreujący fałszywe poczucie bezpieczeństwa.

Konwencje pisania makr

[edytuj]

Ponieważ makra preprocesora działają na zasadzie zwykłego zastępowania napisów, są podatne na wiele kłopotliwych błędów, z których części można uniknąć przez stosowanie się do poniższych reguł:

  1. Umieszczaj nawiasy dookoła argumentów makra kiedy to tylko możliwe. Zapewnia to, że gdy są wyrażeniami kolejność działań nie zostanie zmieniona. Na przykład:
    • Źle: #define kwadrat(x) (x*x)
    • Dobrze: #define kwadrat(x) ( (x)*(x) )
    • Przykład: Załóżmy, że w programie makro kwadrat() zdefiniowane bez nawiasów zostało wywołane następująco: kwadrat(a+b). Wtedy zostanie ono zamienione przez preprocesor na: (a+b*a+b). Z kolejności działań wiemy, że najpierw zostanie wykonane mnożenie, więc wartość wyrażenia kwadrat(a+b) będzie różna od kwadratu wyrażenia a+b.
  2. Umieszczaj nawiasy dookoła całego makra, jeśli jest pojedynczym wyrażeniem. Ponownie, chroni to przed zaburzeniem kolejności działań.
    • Źle: #define kwadrat(x) (x)*(x)
    • Dobrze: #define kwadrat(x) ( (x)*(x) )
    • Przykład: Definiujemy makro #define suma(a, b) (a)+(b) i wywołujemy je w kodzie wynik = suma(3, 4) * 5. Makro zostanie rozwinięte jako wynik = (3)+(4)*5, co - z powodu kolejności działań - da wynik inny niż pożądany.
  3. Jeśli makro składa się z wielu instrukcji lub deklaruje zmienne, powinno być umieszczone w pętli do { ... } while(0), bez kończącego średnika. Pozwala to na użycie makra jak pojedynczej instrukcji w każdym miejscu, jak ciało innego wyrażenia, pozwalając jednocześnie na umieszczenie średnika po makrze bez tworzenia zerowego wyrażenia. Należy uważać, by zmienne w makrze potencjalnie nie kolidowały z argumentami makra.
    • Źle: #define FREE(p) free(p); p = NULL;
    • Dobrze: #define FREE(p) do { free(p); p = NULL; } while(0)
  4. Unikaj używania argumentów makra więcej niż raz wewnątrz makra. Może to spowodować kłopoty, gdy argument makra ma efekty uboczne (np. zawiera operator inkrementacji).
    • Przykład: #define kwadrat(x) ((x)*(x)) nie powinno być wywoływane z operatorem inkrementacji kwadrat(a++) ponieważ zostanie to rozwinięte jako ((a++) * (a++)), co jest niezgodne ze specyfikacją języka i zachowanie takiego wyrażenia jest niezdefiniowane (dwukrotna inkrementacja w tym samym wyrażeniu).
  5. Jeśli makro może być w przyszłości zastąpione przez funkcję, rozważ użycie w nazwie małych liter, jak w funkcji.

Jak dostać się do konkretnego bitu?

[edytuj]

Wiemy, że komputer to maszyna, której najmniejszą jednostką pamięci jest bit, jednak w C najmniejsza zmienna ma rozmiar 8 bitów (czyli jednego bajtu). Jak zatem można odczytać wartość pojedynczych bitów? W bardzo prosty sposób - w zestawie operatorów języka C znajdują się tzw. operatory bitowe. Są to m. in.:

  • & - bitowe "i"
  • | - bitowe "lub"
  • ~ - bitowe "nie"

Oprócz tego są także przesunięcia (<< oraz >>). Zastanówmy się teraz, jak je wykorzystać w praktyce. Załóżmy, że zajmujemy się jednobajtową zmienną.

 unsigned char i = 2;

Z matematyki wiemy, że zapis binarny tej liczby wygląda tak (w ośmiobitowej zmiennej): 00000010. Jeśli teraz np. chcielibyśmy "zapalić" drugi bit od lewej (tj. bit, którego zapalenie niejako "doda" do liczby wartość 26) powinniśmy użyć logicznego lub:

 unsigned char i = 2;
 i |= 64;

Gdzie 64=26. Odczytywanie wykonuje się za pomocą tzw. maski bitowej. Polega to na:

  1. wyzerowaniu bitów, które są nam w danej chwili niepotrzebne
  2. odpowiedniemu przesunięciu bitów, dzięki czemu szukany bit znajdzie się na pozycji pierwszego bitu od prawej

Do "wyłuskania" odpowiedniego bitu możemy posłużyć się operacją "i" - czyli operatorem &. Wygląda to analogicznie do posługiwania się operatorem "lub":

 unsigned char i = 3; /* bitowo: ''00000011'' */
 unsigned char temp = 0;
 temp = i & 1; /* sprawdzamy najmniej znaczący bit - czyli pierwszy z prawej */
 if (temp) {
   printf ("bit zapalony");
 }
 else {
   printf ("bit zgaszony");
 }

Jeśli nie władasz biegle kodem binarnym, tworzenie masek bitowych ułatwią ci przesunięcia bitowe. Aby uzyskać liczbę która ma zapalony bit o numerze n (bity są liczone od zera), przesuwamy bitowo w lewo jedynkę o n pozycji:

 1 << n

Jeśli chcemy uzyskać liczbę, w której zapalone są bity na pozycjach l, m, n - używamy sumy logicznej ("lub"):

 (1 << l) | (1 << m) | (1 << n)

Jeśli z kolei chcemy uzyskać liczbę gdzie zapalone są wszystkie bity poza n, odwracamy ją za pomocą operatora logicznej negacji ~

 ~(1 << n)

Warto władać biegle operacjami na bitach, ale początkujący mogą (po uprzednim przeanalizowaniu) zdefiniować następujące makra i ich używać:

 /* Sprawdzenie czy w liczbie k jest zapalony bit n */
 #define IS_BIT_SET(k, n)     ((k) & (1 << (n)))
 
 /* Zapalenie bitu n w zmiennej k */
 #define SET_BIT(k, n)        (k |= (1 << (n)))
 
 /* Zgaszenie bitu n w zmiennej k */
 #define RESET_BIT(k, n)      (k &= ~(1 << (n)))

Skróty notacji

[edytuj]

Istnieją pewne sposoby ograniczenia ilości niepotrzebnego kodu. Przykładem może być wykonywanie jednej operacji w razie wystąpienia jakiegoś warunku, np. zamiast pisać:

 if (warunek) {
   printf ("Warunek prawdziwy\n");
 }

możesz skrócić notację do:

 if (warunek)
   printf ("Warunek prawdziwy\n");

Podobnie jest w przypadku pętli for:

 for (;warunek;)
   printf ("Wyświetlam się w pętli!\n");

Niestety ograniczeniem w tym wypadku jest to, że można w ten sposób zapisać tylko jedną instrukcję.

Zobacz również

[edytuj]




Przenośność programów

[edytuj]

Jak dowiedziałeś się z poprzednich rozdziałów tego podręcznika, język C umożliwia tworzenie programów, które mogą być uruchamiane na różnych platformach sprzętowych pod warunkiem ich powtórnej kompilacji. Język C należy do grupy języków wysokiego poziomu, które tłumaczone są do poziomu kodu maszynowego (tzn. kod źródłowy jest kompilowany). Z jednej strony jest to korzystne posunięcie, gdyż programy są szybsze i mniejsze niż programy napisane w językach interpretowanych (takich, w których kod źródłowy nie jest kompilowany do kodu maszynowego, tylko na bieżąco interpretowany przez tzw. interpreter). Jednak istnieje także druga strona medalu - pewne zawiłości sprzętu, które ograniczają przenośność programów. Ten rozdział ma wyjaśnić Ci mechanizmy działania sprzętu w taki sposób, abyś bez problemu mógł tworzyć poprawne i całkowicie przenośne programy.[1]

Niezdefiniowane zachowanie i zachowanie zależne od implementacji

[edytuj]

W trakcie czytania kolejnych rozdziałów można było się natknąć na zwroty takie jak zachowanie niezdefiniowane (ang. undefined behavior) czy zachowanie zależne od implementacji (ang. implementation-defined behavior). Cóż one tak właściwie oznaczają?

Zacznijmy od tego drugiego. Autorzy standardu języka C czuli, że wymuszanie jakiegoś konkretnego działania danego wyrażenia byłoby zbytnim obciążeniem dla osób piszących kompilatory, gdyż dany wymóg mógłby być bardzo trudny do zrealizowania na konkretnej architekturze. Dla przykładu, gdyby standard wymagał, że typ unsigned char ma dokładnie 8 bitów to napisanie kompilatora dla architektury, na której bajt ma 9 bitów byłoby cokolwiek kłopotliwe, a z pewnością wynikowy program działałby o wiele wolniej niżby to było możliwe.

Z tego właśnie powodu, niektóre aspekty języka nie są określone bezpośrednio w standardzie i są pozostawione do decyzji zespołu (osoby) piszącego konkretną implementację. W ten sposób, nie ma żadnych przeciwwskazań (ze strony standardu), aby na architekturze, gdzie bajty mają 9 bitów, typ char również miał tyle bitów. Dokonany wybór musi być jednak opisany w dokumentacji kompilatora, tak żeby osoba pisząca program w C mogła sprawdzić jak dana konstrukcja zadziała.

Należy zatem pamiętać, że poleganie na jakimś konkretnym działaniu programu w przypadkach zachowania zależnego od implementacji drastycznie zmniejsza przenośność kodu źródłowego.

Zachowania niezdefiniowane są o wiele groźniejsze, gdyż zaistnienie takowego może spowodować dowolny efekt, który nie musi być nigdzie udokumentowany. Przykładem może tutaj być próba odwołania się do wartości wskazywanej przez wskaźnik o wartości NULL.

Jeżeli gdzieś w naszym programie zaistnieje sytuacja niezdefiniowanego zachowania, to nie jest już to kwestia przenośności kodu, ale po prostu błędu w kodzie, chyba że świadomie korzystamy z rozszerzenia naszego kompilatora. Rozważmy odwoływanie się do wartości wskazywanej przez wskaźnik o wartości NULL. Ponieważ według standardu operacja taka ma niezdefiniowany skutek to w szczególności może wywołać jakąś z góry określoną funkcję - kompilator może coś takiego zrealizować sprawdzając wartość wskaźnika przed każdą dereferencją, w ten sposób niezdefiniowane zachowanie dla konkretnego kompilatora stanie się jak najbardziej zdefiniowane.

Sytuacją wziętą z życia są operatory przesunięć bitowych, gdy działają na liczbach ze znakiem. Konkretnie przesuwanie w lewo liczb jest dla wielu przypadków niezdefiniowane. Bardzo często jednak, w dokumentacji kompilatora działanie przesunięć bitowych jest dokładnie opisane. Jest to o tyle interesujący fakt, iż wielu programistów nie zdaje sobie z niego sprawy i nieświadomie korzysta z rozszerzeń kompilatora.

Istnieje jeszcze trzecia klasa zachowań. Zachowania nieokreślone (ang. unspecified behaviour). Są to sytuacje, gdy standard określa kilka możliwych sposobów w jaki dane wyrażenie może działać i pozostawia kompilatorowi decyzję co z tym dalej zrobić. Coś takiego nie musi być nigdzie opisane w dokumentacji i znowu poleganie na konkretnym zachowaniu jest błędem. Klasycznym przykładem może być kolejność obliczania argumentów wywołania funkcji.

Rozmiar zmiennych

[edytuj]

Rozmiar poszczególnych typów danych (np. int, short czy long) jest różna na różnych platformach, gdyż nie jest definiowany w sztywny sposób (poza typem char, który zawsze zajmuje 1 bajt), jak np. "long int zawsze powinien mieć 64 bity" (takie określenie wiązałoby się z wyżej opisanymi trudnościami), lecz w na zasadzie zależności typu "long powinien być nie krótszy niż int", "short nie powinien być dłuższy od int". Pierwsza standaryzacja języka C zakładała, że typ int będzie miał taki rozmiar, jak domyślna długość liczb całkowitych na danym komputerze, natomiast modyfikatory short oraz long zmieniały długość tego typu tylko wtedy, gdy dana maszyna obsługiwała typy o mniejszej lub większej długości[2].

Z tego powodu, nigdy nie zakładaj, że dany typ będzie miał określony rozmiar. Jeżeli potrzebujesz typu całkowitego o konkretnym rozmiarze (a dokładnej konkretnej liczbie bitów wartości) możesz skorzystać z pliku nagłówkowego :

i używać na przykład:

  • int8_t
  • int16_t
  • int32_t
  • int64_t

zamiast int


stdint.h

[edytuj]

wprowadzonego do języka przez standard ISO C z 1999 roku. Definiuje on typy int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t i uint64_t (o ile w danej architekturze występują typy o konkretnej liczbie bitów).


własny plik nagłówkowy

[edytuj]

Jednak możemy posiadać implementację, która nie posiada tego pliku nagłówkowego. W takiej sytuacji nie pozostaje nam nic innego jak tworzyć własny plik nagłówkowy, w którym za pomocą słówka typedef sami zdefiniujemy potrzebne nam typy. Np.:

 typedef unsigned char      u8;
 typedef   signed char      s8;
 typedef unsigned short     u16;
 typedef   signed short     s16;
 typedef unsigned long      u32;
 typedef   signed long      s32;
 typedef unsigned long long u64;
 typedef   signed long long s64;

Aczkolwiek należy pamiętać, że taki plik będzie trzeba pisać od nowa dla każdej architektury na jakiej chcemy kompilować nasz program.

Porządek bajtów i bitów

[edytuj]

Bajty i słowa

[edytuj]

Wiesz zapewne, że podstawową jednostką danych jest bit, który może mieć wartość 0 lub 1. Kilka kolejnych bitów[3] stanowi bajt (dla skupienia uwagi, przyjmijmy, że bajt składa się z 8 bitów). Często typ short ma wielkość dwóch bajtów i wówczas pojawia się pytanie w jaki sposób są one zapisane w pamięci - czy najpierw ten bardziej znaczący - big-endian, czy najpierw ten mniej znaczący - little-endian.

Skąd takie nazwy? Otóż pochodzą one z książki Podróże Guliwera, w której liliputy kłóciły się o stronę, od której należy rozbijać jajko na twardo. Jedni uważali, że trzeba je rozbijać od grubszego końca (big-endian) a drudzy, że od cieńszego (little-endian). Nazwy te są o tyle trafne, że w wypadku procesorów wybór kolejności bajtów jest sprawą czysto polityczną, która jest technicznie neutralna.

Sprawa się jeszcze bardziej komplikuje w przypadku typów, które składają się np. z 4 bajtów. Wówczas są aż 24 (4 silnia) sposoby zapisania kolejnych fragmentów takiego typu. W praktyce zapewne spotkasz się jedynie z kolejnościami big-endian lub little-endian, co nie zmienia faktu, że inne możliwości także istnieją i przy pisaniu programów, które mają być przenośne należy to brać pod uwagę.

Poniższy przykład dobrze obrazuje oba sposoby przechowywania zawartości zmiennych w pamięci komputera (przyjmujemy CHAR_BIT == 8 oraz sizeof(long) == 4, bez bitów wypełnienia (ang. padding bits)): unsigned long zmienna = 0x00010203; w pamięci komputera będzie przechowywana tak:

adres         | 0  | 1  | 2  | 3  |
big-endian    |0x00|0x01|0x02|0x03|
little-endian |0x03|0x02|0x01|0x00|

Konwersja z jednego porządku do innego

[edytuj]

Czasami zdarza się, że napisany przez nas program musi się komunikować z innym programem (może też przez nas napisanym), który działa na komputerze o (potencjalnie) innym porządku bajtów. Często najprościej jest przesyłać liczby jako tekst, gdyż jest on niezależny od innych czynników, jednak taki format zajmuje więcej miejsca, a nie zawsze możemy sobie pozwolić na taką rozrzutność.

Przykładem może być komunikacja sieciowa, w której przyjęło się, że dane przesyłane są w porządku big-endian. Aby móc łatwo operować na takich danych, w standardzie POSIX zdefiniowano następujące funkcje (w zasadzie zazwyczaj są to makra):

 #include <arpa/inet.h>
 uint32_t htonl(uint32_t);
 uint16_t htons(uint16_t);
 uint32_t ntohl(uint32_t);
 uint16_t ntohs(uint16_t);

Pierwsze dwie konwertują liczbę z reprezentacji lokalnej na reprezentację big-endian (host to network), natomiast kolejne dwie dokonują konwersji w drugą stronę (network to host).

Można również skorzystać z pliku nagłówkowego endian.h, w którym definiowane są makra pozwalające określić porządek bajtów:

 #include <endian.h>
 #include <stdio.h>
 
 int main(void) {
 printf("Porządek ");
 #if __BYTE_ORDER == __BIG_ENDIAN
   printf("big-endian");
 #elif __BYTE_ORDER == __LITTLE_ENDIAN
   printf("little-endian");
 #elif defined __PDP_ENDIAN && __BYTE_ORDER == __PDP_ENDIAN
   printf("PDP");
 #endif
 printf(" (%d)\n", __BYTE_ORDER);
   return 0;
 }

Na podstawie makra __BYTE_ORDER można skonstruować funkcję, która będzie konwertować liczby pomiędzy różnymi porządkami:

 #include <endian.h>
 #include <stdio.h>
 #include <stdint.h>
 
 uint32_t convert_order32(uint32_t val, unsigned from, unsigned to) {
   if (from==to) {
     return val;
   } else {
     uint32_t ret = 0;
     unsigned char tmp[5] = { 0, 0, 0, 0, 0 };
     unsigned char *ptr = (unsigned char*)&val;
     unsigned div = 1000;
     do tmp[from / div % 10] = *ptr++; while ((div /= 10));
     ptr = (unsigned char*)&ret;
     div = 1000;
     do *ptr++ = tmp[to / div % 10]; while ((div /= 10));
     return ret;
   }
 }
 
 #define LE_TO_H(val)  convert_order32((val), 1234, __BYTE_ORDER)
 #define H_TO_LE(val)  convert_order32((val), __BYTE_ORDER, 1234)
 #define BE_TO_H(val)  convert_order32((val), 4321, __BYTE_ORDER)
 #define H_TO_BE(val)  convert_order32((val), __BYTE_ORDER, 4321)
 #define PDP_TO_H(val) convert_order32((val), 3412, __BYTE_ORDER)
 #define H_TO_PDP(val) convert_order32((val), __BYTE_ORDER, 3412)
 #define LE_TO_BE(val) convert_order32((val), 1234, 4321)
 #define BE_TO_LE(val) convert_order32((val), 4321, 1234)
 #define PDP_TO_BE(val) convert_order32((val), 3421, 4321)
 #define PDP_TO_LE(val) convert_order32((val), 3421, 1234)
 #define BE_TO_PDP(val) convert_order32((val), 4321, 3421)
 #define LE_TO_PDP(val) convert_order32((val), 1234, 3421)
 
 int main (void)
 {
   printf("%08x\n", LE_TO_H(0x01020304));
   printf("%08x\n", H_TO_LE(0x01020304));
   printf("%08x\n", BE_TO_H(0x01020304));
   printf("%08x\n", H_TO_BE(0x01020304));
   printf("%08x\n", PDP_TO_H(0x01020304));
   printf("%08x\n", H_TO_PDP(0x01020304));
   return 0;
 }

Ciągle jednak polegamy na niestandardowym pliku nagłówkowym endian.h. Można go wyeliminować sprawdzając porządek bajtów w czasie wykonywania programu:

 #include <stdio.h>
 #include <stdint.h>
 
 int main(void) {
   uint32_t val = 0x04030201;
   unsigned char *v = (unsigned char*)&val;
   int byte_order = *v * 1000 + *(v + 1) * 100 + *(v + 2) * 10 + *(v + 3);
   printf("Porządek ");
   if (byte_order == 4321)
     printf("big-endian");
   else if (byte_order == 1234)
     printf("little-endian");
   else if (byte_order == 3412)
     printf("PDP");
   printf(" (%d)\n", byte_order);
   return 0;
 }

Powyższe przykłady opisują jedynie część problemów jakie mogą wynikać z próby przenoszenia binarnych danych pomiędzy wieloma platformami. Wszystkie co więcej zakładają, że bajt ma 8 bitów, co wcale nie musi być prawdą dla konkretnej architektury, na którą piszemy aplikację. Co więcej liczby mogą posiadać w swojej reprezentacje bity wypełnienia (ang. padding bits), które nie biorą udziały w przechowywaniu wartości liczby. Te wszystkie różnice mogą dodatkowo skomplikować kod. Toteż należy być świadomym, iż przenosząc dane binarnie musimy uważać na różne reprezentacje liczb.

Biblioteczne problemy

[edytuj]

Dostępność bibliotek

[edytuj]

Pisząc programy nieraz będziemy musieli korzystać z różnych bibliotek. Problem polega na tym, że nie zawsze będą one dostępne na komputerze, na którym inny użytkownik naszego programu będzie próbował go kompilować. Dlatego też ważne jest, abyśmy korzystali z łatwo dostępnych bibliotek, które dostępne są na wiele różnych systemów i platform sprzętowych. Zapamiętaj: Twój program jest na tyle przenośny na ile przenośne są biblioteki z których korzysta!

Przykład :

Odmiany bibliotek

[edytuj]

Pod Windows funkcje atan2, floor i fabs są w tej samej bibliotece, co standardowe funkcje C.

Pod Uniksami są w osobnej bibliotece matematycznej libm w wersji:

  • statycznej (zwykle /usr/lib/libm.a) i pliku nagłówkowym math.h (zwykle /usr/include/math.h)[4]
  • ladowanej dynamicznie ( /usr/lib/libm.so )

Aby korzystać z tych funkcji potrzebujemy:

  • dodać : #include <math.h>
  • przy kompilacji dołączyć bibliotekę libm : gcc main.c -lm

Opcja -lm używa libm.so albo libm.a w zależności od tego, które są znalezione, i w zależności od obecności opcji -static. [5][6]


wieloplatformowe

[edytuj]

Kompilacja warunkowa

[edytuj]

Przy zwiększaniu przenośności kodu może pomóc preprocessor. Przyjmijmy np., że chcemy korzystać ze słówka kluczowego inline wprowadzonego w standardzie C99, ale równocześnie chcemy, aby nasz program był rozumiany przez kompilatory ANSI C. Wówczas możemy skorzystać z następującego kodu:

 #ifndef __inline__
 # if __STDC_VERSION__ >= 199901L
 #  define __inline__ inline
 # else
 #  define __inline__
 # endif
 #endif

a w kodzie programu zamiast słówka inline stosować __inline__. Co więcej, kompilator GCC rozumie słówka kluczowe tak tworzone i w jego przypadku warto nie redefiniować ich wartości:

 #ifndef __GNUC__
 # ifndef __inline__
 #  if __STDC_VERSION__ >= 199901L
 #   define __inline__ inline
 #  else
 #   define __inline__
 #  endif
 # endif
 #endif

Korzystając z kompilacji warunkowej można także korzystać z różnego kodu zależnie od (np.) systemu operacyjnego. Przykładowo, przed kompilacją na konkretnej platformie tworzymy odpowiedni plik config.h, który następnie dołączamy do wszystkich plików źródłowych, w których podejmujemy decyzje na podstawie zdefiniowanych makr. Dla przykładu, plik config.h:

 #ifndef CONFIG_H
 #define CONFIG_H
 
 /* Uncomment if using Windows */
 /* #define USE_WINDOWS */
 
 /* Uncomment if using Linux */
 /* #define USE_LINUX */
 
 #error You must edit config.h file
 #error Edit it and remove those error lines
 
 #endif

Jakiś plik źródłowy:

 #include "config.h"
 
 /* ... */
 
 #ifdef USE_WINDOWS
   rob_cos_wersja_dla_windows(void);
 #else
   rob_cos_wersja_dla_linux(void);
 #endif

Istnieją różne narzędzia, które pozwalają na automatyczne tworzenie takich plików config.h, dzięki czemu użytkownik przed skompilowaniem programu nie musi się trudzić i edytować ich ręcznie, a jedynie uruchomić odpowiednie polecenie. Przykładem jest zestaw autoconf i automake.



Łączenie z innymi językami

[edytuj]

Programista, pisząc jakiś program ma problem z wyborem najbardziej odpowiedniego języka do utworzenia tego programu. Niekiedy zdarza się, że najlepiej byłoby pisać program, korzystając z różnych języków. Język C może być z łatwością łączony z innymi językami programowania, które podlegają kompilacji bezpośrednio do kodu maszynowego (Asembler, Fortran czy też C++). Ponadto dzięki specjalnym bibliotekom można go łączyć z językami bardzo wysokiego poziomu (takimi jak np. Python czy też Ruby). Ten rozdział ma za zadanie wytłumaczyć Ci, w jaki sposób można mieszać różne języki programowania w jednym programie.

Język C i Asembler

[edytuj]

Łączenie języka C i języka asemblera jest dość powszechnym zjawiskiem. Dzięki możliwości połączenia obu tych języków programowania można było utworzyć bibliotekę dla języka C, która niskopoziomowo komunikuje się z jądrem systemu operacyjnego komputera. Ponieważ zarówno asembler jak i C są językami tłumaczonymi do poziomu kodu maszynowego, za ich łączenie odpowiada program zwany linkerem (popularny ld). Ponadto niektórzy producenci kompilatorów umożliwiają stosowanie tzw. wstawek asemblerowych, które umieszcza się bezpośrednio w kodzie programu, napisanego w języku C. Kompilator, kompilując taki kod wstawi w miejsce tychże wstawek odpowiedni kod maszynowy, który jest efektem przetłumaczenia kodu asemblera, zawartego w takiej wstawce. Opiszę tu oba sposoby łączenia obydwu języków.

Łączenie na poziomie kodu maszynowego

[edytuj]

W naszym przykładzie założymy, że w pliku f1.S zawarty będzie kod, napisany w asemblerze, a f2.c to kod z programem w języku C. Program w języku C będzie wykorzystywał jedną funkcję, napisaną w języku asemblera, która wyświetli prosty napis "Hello world". Z powodu ograniczeń technicznych zakładamy, że program uruchomiony zostanie w środowisku POSIX na platformie i386 i skompilowany kompilatorem gcc. Używaną składnią asemblera będzie AT&T (domyślna dla asemblera GNU) Oto plik f1.S:

   .text
   .globl _f1
 _f1:
   pushl %ebp
   movl %esp, %ebp
   movl $4, %eax /* 4 to funkcja systemowa "write" */
   movl $1, %ebx /* 1 to stdout */
   movl $tekst, %ecx /* adres naszego napisu */
   movl $len, %edx /* długość napisu w bajtach */
   int $0x80 /* wywołanie przerwania systemowego */
   popl %ebp
   ret
 
   .data
 tekst:
   .string "Hello world\n"
   len = . - tekst

Teraz kolej na f2.c:

 extern void f1 (void); /* musimy użyć słowa extern */
 int main ()
 {
   f1();
   return 0;
 }

Teraz możemy skompilować oba programy:

as f1.S -o f1.o
gcc f2.c -c -o f2.o
gcc f2.o f1.o -o program

W ten sposób uzyskujemy plik wykonywalny o nazwie "program". Efekt działania programu powinien być następujący:

Hello world

Na razie utworzyliśmy bardzo prostą funkcję, która w zasadzie nie komunikuje się z językiem C, czyli nie zwraca żadnej wartości ani nie pobiera argumentów. Jednak, aby zacząć pisać obsługę funkcji, która będzie pobierała argumenty i zwracała wyniki musimy poznać działanie języka C od trochę niższego poziomu.

Argumenty

[edytuj]

Do komunikacji z funkcją język C korzysta ze stosu. Argumenty odkładane są w kolejności od ostatniego do pierwszego. Ponadto na końcu odkładany jest tzw. adres powrotu, dzięki czemu po wykonaniu funkcji program "wie", w którym miejscu ma kontynuować działanie. Ponadto, początek funkcji w asemblerze wygląda tak:

 pushl %ebp
 movl %esp, %ebp

Zatem na stosie znajdują się kolejno: zawartość rejestru EBP, adres powrotu a następnie argumenty od pierwszego do n-tego.

Zwracanie wartości

[edytuj]

Na architekturze i386 do zwracania wyników pracy programu używa się rejestru EAX, bądź jego "mniejszych" odpowiedników, tj. AX i AH/AL. Zatem aby funkcja, napisana w asemblerze zwróciła "1" przed rozkazem ret należy napisać:

 movl $1, %eax

Nazewnictwo

[edytuj]

Kompilatory języka C/C++ dodają podkreślnik "_" na początku każdej nazwy. Dla przykładu funkcja:

void funkcja();

W pliku wyjściowym będzie posiadać nazwę _funkcja. Dlatego, aby korzystać z poziomu języka C z funkcji zakodowanych w asemblerze, muszą one mieć przy definicji w pliku asemblera wspomniany dodatkowy podkreślnik na początku.

Łączymy wszystko w całość

[edytuj]

Pora, abyśmy napisali jakąś funkcję, która pobierze argumenty i zwróci jakiś konkretny wynik. Oto kod f1.S:

 .text
 .globl _funkcja
 _funkcja:
   pushl %ebp
   movl %esp, %ebp
   movl 8(%esp), %eax /* kopiujemy pierwszy argument do %eax */
   addl 12(%esp), %eax /* do pierwszego argumentu w %eax dodajemy drugi argument */
   popl %ebp
   ret /* ... i zwracamy wynik dodawania... */

oraz f2.c:

 #include <stdio.h>
 extern int funkcja (int a, int b);
 int main ()
 {
 printf ("2+3=%d\n", funkcja(2,3));
 return 0;
 }

Po skompilowaniu i uruchomieniu programu powinniśmy otrzymać wydruk: 2+3=5

Wstawki asemblerowe

[edytuj]

Oprócz możliwości wstępnie skompilowanych modułów możesz posłużyć się także tzw. wstawkami asemblerowymi. Ich użycie powoduje wstawienie w miejsce wystąpienia wstawki odpowiedniego kodu maszynowego, który powstanie po przetłumaczeniu kodu asemblerowego. Ponieważ jednak wstawki asemblerowe nie są standardowym elementem języka C, każdy kompilator ma całkowicie odmienną filozofię ich stosowania (lub nie ma ich w ogóle). Ponieważ w tym podręczniku używamy głównie kompilatora GNU, więc w tym rozdziale zostanie omówiona filozofia stosowania wstawek asemblera według programistów GNU.

Ze wstawek asemblerowych korzysta się tak:

 int main ()
 {
   asm ("nop");
 }

W tym wypadku wstawiona zostanie instrukcja "nop" (no operation), która tak naprawdę służy tylko i wyłącznie do konstruowania pętli opóźniających.

Język C++ z racji swojego podobieństwa do C będzie wyjątkowo łatwy do łączenia. Pewnym utrudnieniem może być obiektowość języka C++ oraz występowanie w nim przestrzeni nazw oraz możliwość przeciążania funkcji. Oczywiście nadal zakładamy, że główny program piszemy w C, natomiast korzystamy tylko z pojedynczych funkcji, napisanych w C++. Ponieważ język C nie oferuje tego wszystkiego, co daje programiście język C++, to musimy "zmusić" C++ do wyłączenia pewnych swoich możliwości, aby można było połączyć ze sobą elementy programu, napisane w dwóch różnych językach. Używa się do tego następującej konstrukcji:

 extern "C" {
 /* funkcje, zmienne i wszystko to, co będziemy łączyć z programem w C */
 }

W zrozumieniu teorii pomoże Ci prosty przykład: plik f1.c:

 #include <stdio.h>
 extern int f2(int a);
 
 int main ()
 {
   printf ("%d\n", f2(2));
   return 0;
 }

oraz plik f2.cpp:

 #include <iostream>
 using namespace std;
 extern "C" {
   int f2 (int a)
   {
     cout << "a=" << a << endl;
     return a*2;
   }
 }

Teraz oba pliki kompilujemy:

gcc f1.c -c -o f1.o
g++ f2.cpp -c -o f2.o

Przy łączeniu obu tych plików musimy pamiętać, że język C++ także korzysta ze swojej biblioteki. Zatem poprawna postać polecenia kompilacji powinna wyglądać:

gcc f1.o f2.o -o program -lstdc++

(stdc++ - biblioteka standardowa języka C++). Bardzo istotne jest tutaj to, abyśmy zawsze pamiętali o extern "C", gdyż w przeciwnym razie funkcje napisane w C++ będą dla programu w C całkowicie niewidoczne.

Gnuplot

[edytuj]



Przypisy

  1. Writing portable code by HP AllianceOne Partner Program
  2. Dokładniejszy opis rozmiarów dostępny jest w rozdziale Składnia.
  3. Standard wymaga aby było ich co najmniej 8 i liczba bitów w bajcie w konkretnej implementacji jest określona przez makro CHAR_BIT zdefiniowane w pliku nagłówkowym limits.h
  4. An Introduction to GCC - for the GNU compilers gcc and g++. 2.7 Linking with external libraries
  5. man ld
  6. [c,+gcc+:++atan2,+floor+fabs#4916d793e62da10d | Dyskusja na grupie pl.comp.os.linux.programowanie na temat c, gc : atan2, floor fabs]



Ćwiczenia

[edytuj]

Ćwiczenie 1

[edytuj]

Napisz program, który będzie obliczał wartość funkcji sinus dla kątów , oraz

Ćwiczenie 2

[edytuj]

Napisz program, który:

  1. wczyta ze standardowego wejścia trzy liczby rzeczywiste
  2. wyliczy średnią arytmetyczną tych liczb
  3. obliczy wartość każdej z tych liczb podniesionej do kwadratu
  4. wypisze na standardowe wyjście największą z tych liczb

Ćwiczenie 3

[edytuj]

Wyjaśnij, na czym polega działanie wskaźnika.

Ćwiczenie 4

[edytuj]

Napisz program, który rozpisuje daną liczbę na wszystkie możliwe kombinacje jej składników.

Przykład:

 2 = 1+1
 2 = 2

Inne zadania

[edytuj]

Olimpiada Informatyczna


Składnia

[edytuj]

Symbole i słowa kluczowe

[edytuj]

Język C definiuje pewną ilość słów, za pomocą których tworzy się np. pętle itp. Są to tzw. słowa kluczowe, tzn. nie można użyć ich jako nazwy zmiennej, czy też stałej (o nich poniżej). Oto lista słów kluczowych języka C (według norm ANSI C z roku 1989 oraz ISO C z roku 1990):

Słowo Opis w tym podręczniku
auto Zmienne
break Instrukcje sterujące
case Instrukcje sterujące
char Zmienne
const Zmienne
continue Instrukcje sterujące
default Instrukcje sterujące
do Instrukcje sterujące
double Zmienne
else Instrukcje sterujące
enum Typy złożone
extern Biblioteki
float Zmienne
for Instrukcje sterujące
goto Instrukcje sterujące
if Instrukcje sterujące
int Zmienne
long Zmienne
register Zmienne
return Procedury i funkcje
short Zmienne
signed Zmienne
sizeof Zmienne
static Biblioteki, Zmienne
struct Typy złożone
switch Instrukcje sterujące
typedef Typy złożone
union Typy złożone
unsigned Zmienne
void Wskaźniki
volatile Zmienne
while Instrukcje sterujące

Specyfikacja ISO C z roku 1999 dodaje następujące słowa:

  • bool
  • _Complex
  • _Imaginary
  • inline
  • restrict

Polskie znaki

[edytuj]

Pisząc program, możemy stosować polskie litery (tj. "ąćęłńóśźż") tylko w:

  • komentarzach
  • ciągach znaków (łańcuchach)

Niedopuszczalne jest stosowanie polskich znaków w innych miejscach.

Operatory

[edytuj]

Operatory arytmetyczne

[edytuj]

Są to operatory wykonujące znane wszystkim dodawanie, odejmowanie itp.:

operator znaczenie
+ dodawanie
- odejmowanie
* mnożenie
/ dzielenie
% dzielenie modulo - daje w wyniku samą resztę z dzielenia
= operator przypisania - wykonuje działanie po prawej stronie i wynik przypisuje obiektowi po lewej

Operatory logiczne

[edytuj]

Służą porównaniu zawartości dwóch zmiennych według określonych kryteriów:

Operator Rodzaj porównania
== czy równe
> większy
>= większy bądź równy
< mniejszy
<= mniejszy bądź równy
!= czy różny(nierówny)

Są jeszcze operatory służące do grupowania porównań (patrz też: logika w Wikipedii):

|| lub(OR)
&& i,oraz(AND)
 ! negacja(NOT)

Operatory binarne

[edytuj]

Są to operatory, które działają na bitach.

operator funkcja przykład
| suma bitowa(OR) 5 | 2 da w wyniku 7 ( 00000101 OR 00000010 = 00000111)
& iloczyn bitowy 7 & 2 da w wyniku 2 ( 00000111 AND 00000010 = 00000010)
~ negacja bitowa ~2 da w wyniku 253 ( NOT 00000010 = 11111101 )
>> przesunięcie bitów o X w prawo 7 >> 2 da w wyniku 1 ( 00000111 >> 2 = 00000001)
<< przesunięcie bitów o X w lewo 7 << 2 da w wyniku 28 ( 00000111 << 2 = 00011100)
^ alternatywa wyłączna 7 ^ 2 da w wyniku 5 ( 00000111 ^ 00000010 = 00000101)

Operatory inkrementacji/dekrementacji

[edytuj]

Służą do dodawania/odejmowania od liczby wartości jeden.
Przykłady:

Operacja Opis operacji Wartość wyrażenia
x++ zwiększy wartość w x o jeden wartość zmiennej x przed zmianą
++x zwiększy wartość w x o jeden wartość zmiennej x powiększona o jeden
x-- zmniejszy wartość w x o jeden wartość zmiennej x przed zmianą
--x zmniejszy wartość w x o jeden wartość zmiennej x pomniejszona o jeden

Parę przykładów dla zrozumienia:

int a=7;
if ((a++)==7) /* najpierw porównuje, potem dodaje */ 
  printf ("%d\n",a); /* wypisze 8 */
if ((++a)==9) /* najpierw dodaje, potem porównuje */
  printf ("%d\n", a); /* wypisze 9 */

Analogicznie ma się sytuacja z operatorami dekrementacji.

Pozostałe

[edytuj]
Operacja Opis operacji Wartość wyrażenia
*x operator wyłuskania dla wskaźnika wartość trzymana w pamięci pod adresem przechowywanym we wskaźniku
&x operator pobrania adresu zwraca adres zmiennej
x[a] operator wybrania elementu tablicy zwraca element tablicy o indeksie a (numerowanym od zera)
x.a operator wyboru składnika a ze zmiennej x wybiera składnik ze struktury lub unii
x->a operator wyboru składnika a przez wskaźnik do zmiennej x wybiera składnik ze struktury gdy używamy wskaźnika do struktury zamiast zwykłej zmiennej
sizeof(typ) operator pobrania rozmiaru typu zwraca rozmiar typu w bajtach
sizeof wyrażenie operator pobrania rozmiaru typu zwraca rozmiar typu rezultatu wyrażenia

Operator ternarny

[edytuj]

Istnieje jeden operator przyjmujący trzy argumenty - jest to operator wyrażenia warunkowego: a ? b : c. Zwraca on b gdy a jest prawdą lub c w przeciwnym wypadku.

Typy danych

[edytuj]
Typ Opis Inne nazwy
Typy danych wg norm C89 i C90
char
  • Służy głównie do przechowywania znaków
  • Od kompilatora zależy czy jest to liczba ze znakiem czy bez; w większości kompilatorów jest liczbą ze znakiem
signed char
signed char
  • Typ char ze znakiem
char
unsigned char
  • Typ char bez znaku
short
  • Występuje, gdy docelowa maszyna wyszczególnia krótki typ danych całkowitych, w przeciwnym wypadku jest tożsamy z typem int
  • Często ma rozmiar jednego słowa maszynowego
short int, signed short, signed short int
unsigned short
  • Liczba typu short bez znaku
  • Podobnie, jak short używana do zredukowania zużycia pamięci przez program
unsigned short int
int
  • Liczba całkowita, odpowiadająca podstawowemu rozmiarowi liczby całkowitej w danym komputerze.
  • Podstawowy typ dla liczb całkowitych
signed int, signed
unsigned
  • Liczba całkowita bez znaku
unsigned int, size_t
long
  • Długa liczba całkowita
long int, signed long, signed long int
unsigned long
  • Długa liczba całkowita bez znaku
unsigned long int
float
  • Podstawowy typ do przechowywania liczb zmiennoprzecinkowych
  • W nowszym standardzie zgodny jest z normą IEEE 754
  • Nie można stosować go z modyfikatorem signed ani unsigned
double
  • Liczba zmiennoprzecinkowa podwójnej precyzji
  • Podobnie jak float nie łączy się z modyfikatorem signed ani unsigned
long double
  • Największa możliwa dokładność liczb zmiennoprzecinkowych
  • Nie łączy się z modyfikatorem signed ani unsigned
Typy danych według normy C99
bool
  • Przechowuje wartości 0 lub 1
long long
  • Nowy typ, umożliwiający obliczeniach na bardzo dużych liczbach całkowitych bez użycia typu float
long long int, signed long long, signed long long int
unsigned long long
  • Długie liczby całkowite bez znaku
unsigned long long int
float _Complex
  • Słuzy do przechowywania liczb zespolonych
double _Complex
  • Słuzy do przechowywania liczb zespolonych
long double _Complex
  • Słuzy do przechowywania liczb zespolonych
Typy danych definiowane przez użytkownika
struct
  • Rozmiar zależy od typów danych, umieszczonych w strukturze plus ewentualne dopełnienie[1]
union
  • Rozmiar typu jest taki jak rozmiar największego pola
typedef
  • Nowo zdefiniowany typ przyjmuje taki sam rozmiar, jak typ macierzysty
enum
  • Zwykle elementy mają taką samą długość, jak typ int.

Zależności rozmiaru typów danych są następujące:

  • sizeof(cokolwiek) = sizeof(signed cokolwiek) = sizeof(unsigned cokolwiek);
  • 1 = sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long);
  • sizeof(float) ≤ sizeof(double) ≤ sizeof(long double);
  • sizeof(cokolwiek _Complex) = 2 * sizeof(cokolwiek)
  • sizeof(void *) = sizeof(char *) ≥ sizeof(cokolwiek *);
  • sizeof(cokolwiek *) = sizeof(signed cokolwiek *) = sizeof(unsigned cokolwiek *);
  • sizeof(cokolwiek *) = sizeof(const cokolwiek *).

Dodatkowo, jeżeli przez V(typ) oznaczymy liczbę bitów wykorzystywanych w typie to zachodzi:

  • 8 ≤ V(char) = V(signed char) = V(unsigned char);
  • 16 ≤ V(short) = V(unsigned short);
  • 16 ≤ V(int) = V(unsigned int);
  • 32 ≤ V(long) = V(unsigned long);
  • 64 ≤ V(long long) = V(unsigned long long);
  • V(char) ≤ V(short) ≤ V(int) ≤ V(long) ≤ V(long long).


Przykłady z komentarzem

[edytuj]

Liczby losowe

[edytuj]

Poniższy program generuje wiersz po wierszu macierz o określonych przez użytkownika wymiarach, zawierającą losowo wybrane liczby. Każdy wygenerowany wiersz macierzy zapisywany jest w pliku tekstowym o wprowadzonej przez użytkownika nazwie. W pierwszym wierszu pliku wynikowego zapisano wymiary utworzonej macierzy. Program napisany i skompilowany został w środowisku GNU/Linux.

#include <stdio.h>
#include <stdlib.h>  /* dla funkcji rand() oraz srand() */
#include <time.h>	 /* dla funkcji time() */

main()
{
  int i, j, n, m;
  float re;
  FILE *fp;
  char fileName[128];

  printf("Wprowadz nazwe pliku wynikowego..\n");
  scanf("%s",&fileName);

  printf("Wprowadz po sobie liczbe wierszy i kolumn macierzy oddzielone spacją..\n");
  scanf("%d %d", &n, &m);

           /* jeżeli wystąpił błąd w otwieraniu pliku i go nie otwarto,
            wówczas funkcja fclose(fp) wywołana na końcu programu zgłosi błąd
            wykonania i wysypie nam program z działania, stąd musimy umieścić
            warunek, który w kontrolowany sposób zatrzyma program (funkcja exit;)
           */
  if ( (fp = fopen(fileName, "w")) == NULL )  
  {
    puts("Otwarcie pliku nie jest mozliwe!");
    exit;    /*  jeśli w procedurze glownej
              to piszemy bez nawiasow */
  }

  else  { puts("Plik otwarty prawidłowo..");  }
  
  fprintf(fp, "%d %d\n", n, m);
          /* w pierwszym wierszu umieszczono wymiary macierzy */

  srand( (unsigned int) time(0) );
  for (i=1; i<=n; ++i)
  {
    for (j=1; j<=m; ++j)
    {
      re = ((rand() % 200)-100)/ 10.0;
      fprintf(fp,"%.1f", re );
      if (j!=m) fprintf(fp,"    ");
    }
  fprintf(fp,"\n");
  }  
  fclose(fp);
  return 0;
}

Zamiana naturalnej liczb dziesiętnych na liczby w systemie dwójkowym

[edytuj]

Zajmijmy się teraz innym zagadnieniem. Wiemy, że komputer zapisuje wszystkie liczby w postaci binarnej (czyli za pomocą jedynek i zer). Spróbujmy zatem zamienić liczbę, zapisaną w "naszym" dziesiątkowym systemie na zapis binarny. Uwaga: Program działa jedynie dla liczb od 0 do maksymalnej wartości którą może przyjąć typ unsigned short int w twoim kompilatorze.

#include <stdio.h>
#include <limits.h>

void dectobin (unsigned short a)
{
  int licznik;      

  /* CHAR_BIT to liczba bitów w bajcie */
  licznik = CHAR_BIT * sizeof(a);
  while (--licznik >= 0) {
    putchar (((a >> licznik) & 1) ? '1' : '0');
  }
}

int main ()
{
  unsigned short a;

  printf ("Podaj liczbę od 0 do %hd: ", USHRT_MAX);
  scanf ("%hd", &a);
  printf ("%hd(10) = ", a);
  dectobin (a);
  printf ("(2)\n");

  return 0;
}

Zalążek przeglądarki

[edytuj]

Zajmiemy się tym razem inną kwestią, a mianowicie programowaniem sieci. Jest to zagadnienie bardzo ostatnio popularne. Nasz program będzie miał za zadanie połączyć się z serwerem, którego adres użytkownik będzie podawał jako pierwszy parametr programu, wysłać zapytanie HTTP i odebrać treść, którą wyśle do nas serwer. Zacznijmy może od tego, że obsługa sieci jest niemal identyczna w różnych systemach operacyjnych. Na przykład między systemami z rodziny Unix oraz Windowsem różnica polega tylko na dołączeniu innych plików nagłówkowych (dla Windowsa - winsock2.h). Przeanalizujmy zatem poniższy kod:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>

#define MAXRCVLEN 512
#define PORTNUM 80

char *query = "GET / HTTP1.1\n\n";

int main(int argc, char *argv[])
{
  char buffer[MAXRCVLEN+1];
  int len, mysocket;
  struct sockaddr_in dest; 
  char *host_ip = NULL;
  if (argc != 2) {
    printf ("Podaj adres serwera!\n");
    exit (1);
    }
  host_ip = argv[1];
  mysocket = socket(AF_INET, SOCK_STREAM, 0);

  dest.sin_family = AF_INET; 
  dest.sin_addr.s_addr = inet_addr(host_ip); /* ustawiamy adres hosta */
  dest.sin_port = htons (PORTNUM); /* numer portu przechowuje dwubajtowa zmienna - musimy ustalić porządek sieciowy - Big Endian */
  memset(&(dest.sin_zero), '\0', 8); /* zerowanie reszty struktury */

  connect(mysocket, (struct sockaddr *)&dest,sizeof(struct sockaddr)); /* łączymy się z hostem */
  write (mysocket, query, strlen(query)); /* wysyłamy zapytanie */
  len=read(mysocket, buffer, MAXRCVLEN); /* i pobieramy odpowiedź */

  buffer[len]='\0';

  printf("Rcvd: %s",buffer);
  close(mysocket); /* zamykamy gniazdo */
  return EXIT_SUCCESS;
}

Powyższy przykład może być odrobinę niezrozumiały, dlatego przyda się kilka słów wyjaśnienia. Pliki nagłówkowe, które dołączamy zawierają deklarację nowych dla Ciebie funkcji - socket(), connect(), write() oraz read(). Oprócz tego spotkałeś się z nową strukturą - sockaddr_in. Wszystkie te obiekty są niezbędne do stworzenia połączenia. Aby dowiedzieć się więcej nt. wszystkich użytych tu funkcji i struktur musisz odwiedzić podręcznik o programowaniu w systemie UNIX.

Znajdowanie wzoru funkcji kwadratowej przechodzącej przez trzy punkty

[edytuj]
// gcc m.c -Wall
// ./a.out

# include <stdio.h>

//
//
// znane 3 punkty : (x0,y0), (x1,y1), (x2,y2)
// dane do testów
// (3,10),(1,0),(-2,15) -> f(x)=2x^2 -3x +1   http://matematyka.pisz.pl/strona/1394.html
//   (1,−4), (2,3), (−1,0) -> f(x)=3x2−2x−5  http://matematyka.pisz.pl/forum/52119.html
// A(1,-1), B(0,0), C(-1,3): -> f(x)=x2-2x. http://www.traugutt.miasto.zgierz.pl/matma/kwadratowa.html
// (-6 ,51),(-5 , 38), (-4 , 27) -> The solution is a = 1, b = -2 and c = 3  http://answers.yahoo.com/question/index?qid=20090814183525AA26rSD
// (2 , -8),(1 , 0) (-2 , 0)  -> f(x) = -2(x - 1)(x + 2) =  - 2 x^2  - 2 x + 4  http://www.analyzemath.com/quadraticg/Tutorial1.html
// (1, 3), (−1, 4),  (2, 1) -> a = −1/2 , b = −1/2 , and c = 4 
// (−1,2), (0,3) and (1,6) -> y=x2+2x+3 http://www.amsi.org.au/ESA_Senior_Years/SeniorTopic2/2a/2a_2content_8.html



double wx[3]={3,1,-2}; // double wx[3]={x0,x1,x2};
double wy[3]={10,0,15}; // double wy[3]={y0,y1,y2};

// szukamy trójmianu kwadratowego 
// f(x) = ax^2 + bx + c 
//przechodzącego przez te 3 punkty = interpolacja kwadratowa
//
// układ 3 równań liniowych 
// z 3 niewiadomymi : a, b, c 
// a*x0^2 + b*x0 + c = y0
// a*x1^2 + b*x1 + c = y1
// a*x2^2 + b*x2 + c = y2



// układ 3 równań liniowych postaci :
// a*wi1 + b*wi2 + c*wi3 = yi
//
// win jest współczynnikiem : w 
// gdzie pierwsza cyfra i jest numerem równia ( rząd )
// druga cyfra n jest numerem kolumny ( szukanej a, b, c ) 
//
// a*w00 + b*w01 + c*w02 = y0
// a*w10 + b*w11 + c*w12 = y1
// a*w20 + b*w21 + c*w22 = y2

// wi0 = xi^2
// wi1 = xi
// wi2 = 1




// 4 macierze : 
//double ws[3][3] = {{w00,w01,w02},{w10,w11,w12}, {w20,w21,w22}};
// w kolumnie n jest yi
//double wa[3][3] = {{y0,w01,w02},{y1,w11,w12}, {y2,w21,w22}};
//double wb[3][3] = {{w00,y0,w02},{w10,y1,w12}, {w20,y2,w22}};
//double wc[3][3] = {{w00,w01,y0},{w10,w11,y1}, {w20,w21,y2}};

// a=wa/ws
// b= wb/ws
// c=wc/ws
 
 
 
 
 /*
w = wyznacznik macierzy 3x3 

1 2 3		w11 w12 w13 	a1 b1 c1  	w00 w01 w02 
4 5 6		w21 w22 w23	a2 b2 c2	w10 w11 w12 
7 8 9		w31 w32 w33	a3 b3 c3	w20 w21 w22 

det = 1*5*9       + 2*6*7       + 3*4*8       - 3*5*7       - 2*4*9       - 1*6*8 =  
det = w11*w22*w33 + w21*w32*w13 + w31*w12*w23 - w31*w22*w13 - w11*w32*w23 - w21*w12*w33;
det = a1*b2*c3    + a2*b3*c1    + a3*b1*c2    - a3*b2*c1    - a1*b3*c2    - a2*b1*c3;
det = w00*w11*w22 + w10*w21*w02 + w20*w01*w12 - w20*w11*w02 - w00*w21*w12 - w10*w01*w22;
  


http://www.sciaga.pl/tekst/41303-42-obliczanie_wyznacznikow_macierzy_c_c

test : 
1 2 3
6 5 4
3 7 2
----
63.0000


http://www.matematyka.pl/12881.htm
3 1 1
2 2 3
1 3 2
----
-12

*/
// macierze do testów funkcji DeterminatOfMatrix33
double t1[3][3] = {{1,2,3},{6,5,4},{3,7,2}}; // det = 63
double t2[3][3] = {{3,1,1},{2,2,3},{1,3,2}}; // det = -12
// w = 1*5*9       + 2*6*7       + 3*4*8       - 3*5*7       - 2*4*9       - 1*6*8 =  0
double t3[3][3] = {{1,2,3},{4,5,6},{7,8,9}}; // det = 0
double t4[3][3] = {{2,1,1},{1,-1,-1},{1,2,1}}; // det = 3  http://www.purplemath.com/modules/cramers.htm


double DeterminatOfMatrix33(double w[3][3])
{
return ( w[0][0]*w[1][1]*w[2][2] + w[1][0]*w[2][1]*w[0][2] + w[2][0]*w[0][1]*w[1][2] - w[2][0]*w[1][1]*w[0][2] - w[0][0]*w[2][1]*w[1][2] - w[1][0]*w[0][1]*w[2][2] );
  
}

// i =0 give det(wa); 
// i =1 give det(wb)
// i =2 give det(wc)
double GiveDet_n(int n, double ws[3][3], double wy[3])
{
  int i;
  double wi[3][3]; // use local copy, do not change ws !
  
 
 // copy values from ws to wi
 for (i=0; i<3; ++i)
   {
   wi[i][0]= ws[i][0];
   wi[i][1]= ws[i][1];      
   wi[i][2]= ws[i][2];		
  }
  
  // copy wy column
 for (i=0; i<3; ++i)
   wi[i][n]=wy[i];
   
 //
 printf(" w%d = {",n);
 for (i=0; i<3; ++i)
  {
   printf("{ %f ,",wi[i][0]);
   printf("%f ,",wi[i][1]);
   printf("%f }, \n",wi[i][2]);
  }  
   
   return DeterminatOfMatrix33(wi);
}


// main matrix of system of equations 
double GiveMatrixOfSystem(double wx[3], double ws[3][3])
{
 int i;
 
 printf(" ws = {");
 for (i=0; i<3; ++i)
  {
   ws[i][0]= wx[i]*wx[i]; printf("{ %f ,",ws[i][0]);
   ws[i][1]= wx[i];       printf("%f ,",ws[i][1]);
   ws[i][2]= 1;		  printf("%f }, \n",ws[i][2]);
  }
  
  

return DeterminatOfMatrix33(ws);
}


/* =================== main ============================================================*/

int main()
{

 double ws[3][3];
 double dets,deta, detb, detc;
  double a,b,c;
 
 dets = GiveMatrixOfSystem(wx,ws);
 deta = GiveDet_n(0,ws,wy);
 detb = GiveDet_n(1,ws,wy);
 detc = GiveDet_n(2,ws,wy);
 
 a = deta/dets;
 b = detb/dets;
 c = detc/dets;
 

 printf("a = %f ; b = %f ; c = %f ;\n",a,b,c);
 
 
 return 0;
}

Wybieranie ciągu z łańcucha

[edytuj]
#include <stdio.h> // printf
/*
 gcc r.c -Wall
 time ./a.out


'0123456789012345678901'
'2345678'

/*


http://stackoverflow.com/questions/9895216/remove-character-from-string-in-c

"The idea is to keep a separate read and write pointers (pr for reading and pw for writing), 
always advance the reading pointer, and advance the writing pointer only when it's not pointing to a given character."

modified 



 remove first length2rmv chars and after that take only length2stay chars from input string
 output = input string 
*/
void extract_str(char* str, unsigned int length2rmv, unsigned long int length2stay) {
    // separate read and write pointers 
    char *pr = str; // read pointer
    char *pw = str; // write pointer
    int i =0; // index

    while (*pr) {
        if (i>length2rmv-1 && i <length2rmv+length2stay)
          pw += 1; // advance the writing pointer only when 
        pr += 1;  // always advance the reading pointer
        *pw = *pr;    
        i +=1;
    }
    *pw = '\0';
}

int main() {
    char str[] = "0123456789012345678901";
    printf("'%s'\n", str);
    extract_str(str, 2, 7); // opuszczamy 2 pierwsza znaki i wybieramy 7 następnych
    printf("'%s'\n", str);
    return 0;
}

Liczby wymierne binarne

[edytuj]
Zastosowanie liczb wymiernych binarnych do tworzenia grafiki
#include <stdio.h> // 	fprintf



/*
https://stackoverflow.com/questions/19738919/gcd-function-for-c
The GCD function uses Euclid's Algorithm. 
It computes A mod B, then swaps A and B with an XOR swap.
*/

int gcd(int a, int b)
{
    int temp;
    while (b != 0)
    {
        temp = a % b;

        a = b;
        b = temp;
    }
    return a;
}



int main(){

	int dMax = 6;
	int denominator = 1;
	
	
	fprintf(stdout, "0/%d\n", denominator); // initial value
	for (int d = 0; d < dMax; ++d ){
 		denominator *=2; 
        	for (int n = 1; n < denominator; ++ n ){
   	    		if (gcd(n,denominator)==1 )// irreducible fraction
   	    			{fprintf(stdout, "%d/%d\t", n,denominator);}
   	    			 }// n
   	    		fprintf(stdout, "\n"); // end of the line
   	    		
   	    		} // d
   	return 0;
}

Kompilujemy:

 gcc d.c -Wall -Wextra
 

Wynik :

./a.out 
0/1
1/2	
1/4	3/4	
1/8	3/8	5/8	7/8	
1/16	3/16	5/16	7/16	9/16	11/16	13/16	15/16	
1/32	3/32	5/32	7/32	9/32	11/32	13/32	15/32	17/32	19/32	21/32	23/32	25/32	27/32	29/32	31/32	
1/64	3/64	5/64	7/64	9/64	11/64	13/64	15/64	17/64	19/64	21/64	23/64	25/64	27/64	29/64	31/64	33/64	35/64	37/64	39/64	41/64	43/64	45/64	47/64	49/64	51/64	53/64	55/64	57/64	59/64	61/64	63/64




Przypisy

[edytuj]
  1. Patrz - rozdział Więcej o kompilowaniu.