Mikrokontrolery - Jak zacząć?

... czyli zbiór praktycznej wiedzy dot. mikrokontrolerów.
Pokazywanie postów oznaczonych etykietą Kurs C. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Kurs C. Pokaż wszystkie posty

sobota, 9 kwietnia 2011

Compound literals, czyli jak wygodnie przekazywać parametry złożone

Autor: tmf
Redakcja: Dondu

Kurs języka C: Spis treści

Jakiś czas temu na Elektrodzie pojawiło się pytanie o to, w jaki sposób wygodnie przekazywać do funkcji złożony argument.

O co chodzi?
Spójrzmy na prototyp poniższej funkcji:
void foo(int *ptr);
Nasza funkcja foo przyjmuje jako argument wskaźnik na int. Skoro musimy jej przekazać wskaźnik, to znaczy, że jej wywołanie musi wyglądać tak jak poniżej:
int var=10;
foo(&var);
Wszystko pięknie, ale w powyższym trywialnym przykładzie, aby funkcji przekazać wartość 10, musimy utworzyć tymczasową zmienną var, nadać jej wartość, a następnie przekazać adres zmiennej var do funkcji foo. Z oczywistych powodów konstrukcje typu:
foo(&10);
lub też:
foo(10);
nie zadziałają wcale (błędy podczas kompilacji) lub nie zrobią tego, czego oczekujemy. Jak ten problem rozwiązać? Standard C99 wprowadza nam eleganckie rozwiązanie powyższego problemu pod postacią tzw. literałów złożonych (compound literals). Na czym one polegają? Ogólnie zamiast stworzyć tymczasowe zmienne poza wywołaniem funkcji, tak jak to pokazałem w powyższym przykładzie, możemy utworzyć tymczasowe „obiekty” w samym wywołaniu funkcji:
foo(&(int){10});

Jak widzimy zastosowanie takiego literału polega na podaniu w nawiasie okrągłym jego typu, a następnie w nawiasie klamrowym inicjalizatora typu – czyli po prostu wartości tworzonego obiektu.

Pamiętaj, że o ile zapis z wykorzystaniem literału złożonego jest krótszy i bardziej elegancki, to współczesne kompilatory są na tyle sprawne, że zarówno zapis z literałem złożonym, jak i zapis wykorzystujący zmienne tymczasowe powinien wygenerować identyczny kod asemblerowy. Stąd też stosowanie literałów złożonych w dużej mierze związane jest tylko z większą czytelnością zapisu kodu.

Obiekt będący literałem złożonym istnieje tylko w czasie wywołania funkcji foo – po jej zakończeniu jest automatycznie niszczony, nie możemy się już do niego odwoływać. Korzyści ze stosowania tego typu literałów jeszcze wyraźniej widać w przypadku struktur danych. Rzućmy okiem na kolejny przykład:

typedef union
{
   struct
   {
      uint8_t IB02      : 3;
      uint8_t AM        : 1;
      uint8_t ID        : 2;
      uint8_t TY        : 2;
      uint8_t DMode     : 1;
      uint8_t NoSync    : 1;
      uint8_t WMode     : 1;
      uint8_t DenMode   : 1;
      uint8_t IB12      : 1;
      uint8_t DFM       : 2;
      uint8_t VSMode    : 1;
   };
   uint16_t word;
} ssd2119_EntryMode_Reg;

Zdefiniowaliśmy sobie unię struktury anonimowej i typu uint16_t, który będzie reprezentował 16-bitową wartość jej pól. Dzięki temu mamy wygodny dostęp do poszczególnych pól (jak łatwo zauważyć pola te definiują bity sterujące jednego z rejestrów kontrolera SSD2119), a pole word będzie zawierać bieżącą reprezentację binarną całej struktury. Zadeklarujmy sobie teraz prototyp funkcji wykorzystującej powyższą unię:
void ssd2119_SendCmdWithData(uint8_t cmd, uint16_t data);
Jak widzimy, nasza unia przekazywana jest do funkcji jako argument data o typie uint16_t, a nie ssd2119_EntryMode_Reg. Dlaczego tak? Nasza funkcja ssd2119_SendCmdWithData wysyła do kontrolera LCD po prostu polecenie przekazywane jako argument cmd, a pole data zawiera tylko dane, które razem z poleceniem wysyłamy. Dane te są zależne od wybranego polecenia, w związku z tym ich typ może być inny niż ssd2119_EntryMode_Reg. Mamy już strukturę danych, a także funkcję wysyłającą polecenie do kontrolera, połączmy to teraz w całość. Klasycznie, polecenie zapisu do rejestru Entry Mode kontrolera wysłalibyśmy tworząc zmienną tymczasową:
ssd2119_EntryMode_Reg tmp={.DFM=0b11, .DenMode=1, .TY=0b01, .ID=0b11, .AM=0};
ssd2119_SendCmdWithData(0x11, tmp.word);
Ale możemy też to uczynić z wykorzystaniem literałów złożonych:
ssd2119_SendCmdWithData(0x11, (ssd2119_EntryMode_Reg){.DFM=0b11, .DenMode=1, .TY=0b01, .ID=0b11, .AM=0}.word);
W tym przypadku utworzony został obiekt tymczasowy o typie ssd2119_EntryMode_Reg, który inicjalizujemy analogicznie jak to robimy w przypadku inicjalizacji struktur/unii, a do funkcji, która oczekuje przecież typu uint16_t, przekazujemy wartość pola word reprezentującego naszą unię. Proste, prawda? W ten sposób uzyskujemy o wiele krótszy zapis i moim zdaniem bardziej przejrzysty.

Wykorzystanie takich literałów daje także inne korzyści, m.in. możliwość łatwej inicjalizacji typów złożonych, np. list – co wykorzystujemy np. podczas tworzenia menu i obiektów związanych z grafiką. Liczne przykłady wykorzystujące literały złożone wykorzystuję w swoich książkach „AVR. Praktyczne projekty” i „AVR. Układy peryferyjne”. Można się z nimi zapoznać pobierając darmowe przykłady (ze strony wydawnictwa Helion). Warto to zrobić, bo jak widać, literały złożone to prosty sposób na tworzenie przejrzystych programów. Chodzi tu nie tylko o uzyskaną zwartość kodu, ale także o niezaśmiecanie przestrzeni nazw zmiennymi tymczasowymi, co pozwala nam uniknąć wielu problemów.

Kurs języka C: Spis treści

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

czwartek, 10 lutego 2011

Kurs C: Wskaźniki - Operator adresu &

Autor: Dondu

Kurs języka C: Spis treści


Wskaźniki to jeden z najważniejszych elementów języka C. Umożliwiają one pisanie bardzo efektywnych programów, co w szczególności jest istotne w przypadku mikrokontrolerów, gdzie zależy nam na oszczędzaniu pamięci programu. Wskaźniki mogą być wykorzystywane zarówno przy operowaniu na pamięci RAM (np. SRAM) jak i ROM (np. FLASH).

Część początkujących unika wskaźników jak ognia nie rozumiejąc istoty i zawiłości z nimi związanych. Jest to zrozumiałe, bo faktycznie na początku drogi łatwo popełniać błędy. Dlatego też wskaźniki są naturalnym kolejnym, ale bardzo istotnym stopniem wtajemniczenia, na którego poznanie należy i warto poświęcić nieco czasu.

Zacznijmy więc od tego, że:

Deklaracja zmiennej rezerwuje miejsce w pamięci, czyli kompilator przydziela dla niej określony (jej typem) obszar w pamięci (jeden lub kilka bajtów).

Jako, że każda komórka pamięci ma swój adres, stąd każda zmienna takowy adres posiada.

Aby uzyskać adres zmiennej przydzielony jej przez kompilator należy użyć:

Operator adresu: &

Poniższy przykład pozwala na wyświetlenie adresu, pod którym znajduje się zadeklarowana zmienna a. W celu poznania adresu wykorzystamy operator adresu (symbol &), a do jego wyświetlenia wykorzystamy funkcję printf wraz z formatowaniem adresów wskaźników, czyli %p. (więcej na temat printf w artykule: Printf() i funkcje pochodne).

Aby więc uzyskać adres zmiennej a, należy użyć jej wraz z operatorem adresu, czyli: &a

Przykład 1 (w kompilatorze)
#include <stdio.h>

unsigned char a;  //deklaracja zmiennej 

int main(void){

  printf("adres zmiennej a: %p \n", &a); //pokaż adres zmiennej a

  return 0;
}

Zobacz jakie będą adresy zmiennych deklarowanych w funkcji i poza nią:

Przykład 2 (w kompilatorze)
#include <stdio.h>

unsigned char a, b;

int main(void){

  unsigned char c, d;

  printf("adres zmiennej a: %p \n", &a); //zmienna w obszarze 1
  printf("adres zmiennej b: %p \n", &b); //zmienna w obszarze 1

  printf("adres zmiennej c: %p \n", &c); //zmienna w obszarze 2
  printf("adres zmiennej d: %p \n", &d); //zmienna w obszarze 2

  return 0;
}

Po skompilowaniu w CManiaku powyższego przykładu zauważysz, że zmienne deklarowane są w dwóch różnych obszarach pamięci. Gdy kompilowałem powyższy program w trakcie pisania artykułu adresy były następujące:

adres zmiennej a: 0x80497cd
adres zmiennej b: 0x80497cc
adres zmiennej c: 0xbfc12f9e
adres zmiennej d: 0xbfc12f9f


Zauważ że:
  • zmienne deklarowane są w dwóch różnych obszarach pamięci w zależności od tego, czy były deklarowane w funkcji, czy poza nią,
  • adresy zmiennych a oraz b są malejące, a zmiennych c oraz d rosnące,
  • po niewielkiej zmianie kodu programu i nowej kompilacji adresy zmiennych mogą być inne (decyduje o tym kompilator).

Teraz już wiesz dlaczego wskaźniki nazywają się wskaźnikami - po prostu wskazują położenie zmiennej w pamięci.

Wskaźniki mogą wskazywać nie tylko położenie zmiennych, ale także np. adres funkcji:

Przykład 3 (w kompilatorze)
#include <stdio.h>

void funkcja(void){
 
 //jakis kod
 
 return;
}

int main(void){

  printf("adres funkcji funkcja(): %p \n", &funkcja);
  printf("adres funkcji main(): %p \n", &main);

  return 0;
}

Ale po co znać adres funkcji? Na przykład po to, by móc skatalogować adresy funkcji w tablicy i wywoływać funkcje w zależności od jakiegoś parametru, ale o tym napiszę w jednym z kolejnych artykułów.


Kurs języka C: Spis treści

42
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs C: Proces kompilacji programu


Autor: Dondu

Kurs języka C: Spis treści

Edycja kodu programu
Zanim powstanie ostateczny program wynikowy, musimy go oczywiście napisać w jakimś edytorze. Najlepiej, gdy jest to Zintegrowane Środowisko Programistyczne (tzw. IDE).


Proces kompilacji
Proces kompilacji programu, składa się z kilku wzajemnie powiązanych czynności.


Rys. Proces kompilacji (źródło: xion.org.pl)

Preprocesor
Proces ten ma przygotować właściwy kod C, który będzie podlegał kompilacji, ponieważ kod kompilowanego programu może zawierać szereg dyrektyw oraz linkowanych plików nagłówkowych, stąd niezbędne jest przygotowanie "właściwego i ostatecznego" kodu w języku C, który podlegać będzie kompilacji.


Kompilacja
Właściwym procesem kompilacji przygotowanych przez preprocesor plików z kodem źródłowym C zajmuje się kompilator. Kompilacja polega na przekształceniu kodu C na język maszynowy procesora (np. mikrokontrolera). Każdy plik kompilowany jest osobno.


Linkowanie
Skompilowane moduły (pliki źródłowe języka C oraz biblioteki) muszą zostać połączone w jeden spójny plik kodu maszynowego. Tym procesem zajmuje się tzw. Linker (pol. konsolidator).


Kurs języka C: Spis treści

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs C: Dyrektywa #include


Autor: Dondu

Kurs języka C: Spis treści

Dyrektywa #include odpowiada za wstawienie w jej miejsce wskazanego jako jej argument pliku nagłówkowego. Pliki te łączone są przez preprocesor przed właściwym procesem kompilacji.



Pliki nagłówkowe zawierają z reguły deklaracje zmiennych, funkcji, klas i innych struktur danych. Pliki nagłówkowe z reguły występują w parze z plikiem programu. Na przykład gdy stosujemy gotową bibliotekę do obsługi wyświetlacza LCD, będziemy mieli dwa pliki o przykładowych nazwach:
  • LCD.c (zawiera właściwy kod obsługi wyświetlacza LCD)
  • LCD.h (zawiera deklaracje zmiennych, funkcji, klas i innych struktur danych).

Jednakże w programie wykorzystującym tę bibliotekę, linkujemy za pomocą dyrektywy #include jedynie plik LCD.h (plik nagłówkowy). Pliku LCD.c nie linkujemy!


Nazwę pliku nagłówkowego podaje się na dwa sposoby.



Sposób 1 - Plik wskazany pomiędzy znakami < oraz >

#include <nazwa pliku>

Tak wskazany plik oznacza dla preprocesora, że powinien szukać pliku o podanej nazwie we własnych plikach nagłówkowych. Na przykład:
#include <stdio.h>
#include <string.h>



Sposób 2 - Plik wskazany pomiędzy cudzysłowami

#include "nazwa pliku"

Tak wskazanego pliku, preprocesor szukać będzie w katalogu, w którym znajduje się kompilowany plik zawierający tak napisaną dyrektywę #include.


Ścieżki do plików

W obu sposobach możesz podawać nazwę pliku wraz ze ścieżką względną lub bezwzględną na normalnych zasadach obowiązujących w systemie, na którym działa preprocesor (np. Linux, Windows, itp).
#include <avr/io.h>

#include "/user/include/moj_plik.h"
#include "../user/include/moj_plik.h"
#include "C:\\MojeProjekty\projekt1\moj_plik.h"


Kurs języka C: Spis treści

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs C: Preprocesor


Autor: Dondu

Kurs języka C: Spis treści

Preprocesor, to proces wchodzący w skład całego procesu kompilacji programu. Proces ten ma przygotować właściwy kod C, który będzie podlegał kompilacji.

Ale o co chodzi?!


Przykład 1

Wyobraź sobie sytuację, w której masz program, z którego chciałbyś zrobić wersję demonstracyjną mającą pewne ograniczenia. Możesz skopiować pełną wersję i usunąć na stałe te fragmenty, których nie chciałbyś, by były w wersji demonstracyjnej programu.

W ten sposób masz dwie różne wersje źródłowe. Skompilujesz obie i hulaj dusza!

A co gdy będziesz musiał poprawić fragment programu, który jest zarówno w wersji pełnej jak i demonstracyjnej? Będziesz musiał dokonać poprawek w obu źródłach. Koszmar!!!


Ratunek tkwi w Preprocesorze

I tutaj z pomocą przychodzi nam preprocesor, oferując zestaw dyrektyw oraz poprzedzając proces właściwej kompilacji dodatkowym procesem przygotowującym właściwy kod C.


Używając dyrektyw preprocesora, możesz więc tak napisać program, że z jednego kodu źródłowego będziesz mógł wygenerować zarówno kod pełnej wersji programu jak i demonstracyjnej, a ewentualne poprawki kodu będziesz już wykonywać tylko w jednym kodzie. Genialne!


Przykład 2

W mikrokontrolerach często piszemy programy na różne mikrokontrolery tej samej rodziny. Często też w nowych projektach wykorzystujemy napisane wcześniej fragmenty programów.

Mając możliwość korzystania z preprocesora, możemy tak napisać program, że łatwo zmienimy podłączenie np. wyświetlacza LCD do innych pinów tego samego lub innego mikrokontrolera. To zaoszczędza czas potrzebny na dokonywanie zmian w wielu liniach programu.


Powyższe przykłady, to oczywiście tylko część z wielu możliwości jakie daje nam Preprocesor. Wszystkie omówimy w osobnych artykułach. Poniżej jedynie wykaz czym się zajmiemy. 


Dyrektywy preporcesora

Lista dyrektyw.

#include
#define
#undef

#if #elif #else #endif
#ifdef #ifndef #else #endif

#error
#warning

#line


Makra

Makra są poniekąd odpowiednikami funkcji inline. ale dostępnymi z poziomu preprocesora. Do czego służą i jak je wykorzystywać, opiszę w osobnym artykule.


Makra predefiniowane

To makra zawierające konkretne informacje, które można wykorzystać w dowolny sposób:

__DATE__ - data kompilacji
__TIME__ - godzina kompilacji
__FILE__ - nazwa kompilowanego pliku
__LINE__ - numer linijki kodu



Kurs języka C: Spis treści

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs C: Funkcje - deklaracja


Autor: Dondu

Kurs języka C: Spis treści

Gdy kompilator przygotowuje wynikowy kod maszynowy, może zaprotestować napotykając się na użycie funkcji, która nie została jeszcze zdefiniowana. W szczególności ten przypadek może się zdarzyć, gdy definiujesz funkcję w dalszych liniach kodu niż funkcję ją wywołującą.

W takiej sytuacji należy wcześniej poinformować kompilator, że taka funkcja istnieje. Robimy to za pomocą deklarowania funkcji. Deklarowanie funkcji polega na przekopiowaniu pierwszej linijki funkcji i zakończeniu jej średnikiem.

W poniższym przykładzie pokazałem, co jest deklaracją, a co definicją funkcji obliczenia().

Przykład 1 (w kompilatorze)
//deklaracja funkcji
int obliczenia(int a, int b, int c);


//program zaczyna się od funkcji main()
int main(void)
{
  
  //oblicz i wdrukuj
  printf( "a*b+c = %d \n", obliczenia(5, 23, 9) );

  return 0;  //zakończ wykonywanie programu
}


//definicja funkcji
int obliczenia(int a, int b, int c){
  return a*b+c;
}


Ale kompilator czasami protestuje:

implicit declaration of function
(niejawna deklaracja funkcji)

Jeżeli w trakcie kompilacji natkniesz się na warning o treści:

warning: implicit declaration of function ‘JakasFunkcja’

to właśnie oznacza problem braku deklaracji funkcji.

Poniżej przykład programu, który wygeneruje taki właśnie warning.

Przykład 2 (w kompilatorze)
//deklaracja funkcji
//char JakasFunkcja(int a);
  
int main()
{
 
  int z;
  for(z=0; z<10; z++){
     JakasFunkcja(z);
  }
 
  return 0;
}
 
//definicja funkcji
char JakasFunkcja(int a){
   printf( "%d \n", a);
}

W CManiak'u w powyższym przykładzie znajdziesz zakomentowaną deklarację funkcji. Usuń komentarz, a przekonasz się, że kompilacja przebiegnie poprawnie.

Warningi trzeba eliminować!
Więcej na ten temat przeczytasz tutaj: Błędy kompilacji programu


Kurs języka C: Spis treści

35
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs C: Funkcje inline


Autor: Dondu

Kurs języka C: Spis treści

Normalne funkcje przez kompilator zamieniane są na fragmenty kodu maszynowego i umieszczane w innych fragmentach pamięci programu niż program go wywołujący. Wywołanie takiej funkcji powoduje wykonanie skoku do tego fragmentu pamięci programu. Takie funkcje z reguły na początku posiadają rozkazy umieszczające na stosie (w pamięci ram)  wartości rejestrów wykorzystywanych przez daną funkcję, a ściągają je ze stosu podczas wychodzenia z funkcji rozkazem powrotu.


inline = In Line 
(ang. w linii)

Funkcje inline to funkcje, które w wynikowym kodzie maszynowym wstawiane są w miejsce, gdzie użyłeś je w kodzie C. Nie zawierają zatem rozkazów umieszczania rejestrów na stosie, ani ściągania z niego. Nie zawierają także rozkazów powrotu z funkcji.

Skoro funkcja jest wstawiana w kodzie maszynowym w miejscu jej użycia w kodzie C, to powstaje pytanie, jak to jest realizowane, gdy mam zdefiniowaną jedną funkcję inline, a użyję jej w kilku miejscach programu?

Odpowiedź jest prosta: Kompilator wstawi kod funkcji w każde miejsce, w którym zostanie użyta, przez co wynikowy kod maszynowy będzie z reguły większy niż przy zastosowaniu zwykłych funkcji.

Stąd płynie także wniosek, że w większości przypadków funkcje inline powinny być krótkie, czyli realizować bardzo małe fragmenty kodu.



Return w funkcji inline

Return w funkcji inline ma dla programisty C dokładnie takie samo znaczenie, jak w zwykłych funkcjach. Różnica polega na tym, że w kodzie maszynowym nie będzie umieszczony rozkaz powrotu z funkcji, ponieważ nigdzie nie wykonaliśmy skoku, ale efekt będzie taki jak napisałeś w kodzie C.

Ale tym sobie głowy nie zaprzątaj, bo dla Ciebie nie ma to większego znaczenia, a użycie w funkcji inline instrukcji return upodabnia je do zwykłych, przez co ułatwia pisanie kodu.



Kiedy stosować funkcje inline?

Przede wszystkim w miejscach, w których zależy nam na szybkości działania programu, ponieważ nie tracimy w ten sposób czasu na operacjach skoku do funkcji, operowania na stosie i powrotu z funkcji.



Poniżej przykład funkcji inline.

Przykład 1 (w kompilatorze)
inline int obliczenia(int a, int b, int c){
  //funkcja oblicza wartość według wzoru: y = a*b + c
  int y;
  y = a*b;
  y = y + c;
  return y;
  
}

int main(void)
{
  
  int a = 5;
  int b = 15;
  int c = 3;
  
  printf( "a*b + c = %d \n", obliczenia(a, b, c) );

  a = 80;
  printf( "a*b + c = %d \n", obliczenia(a, b, c) );
  
  a = 97;
  c = 19;
  printf( "a*b + c = %d \n", obliczenia(a, b, c) );
  
  return 0;
}

Niestety w kompilatorze CManiak nie zauważysz różnicy pomiędzy funkcją zwykłą, a funkcją inline, ponieważ nie pokazuje on wynikowego kodu maszynowego.

Dlatego poniżej pokazuję kod odpowiadający powyższemu kodowi, tak jak można zrozumieć działanie kompilatora, polegające na podstawieniu kodu funkcji inline w miejscach, gdzie w kodzie przykładu 1 wywoływaliśmy funkcję.

Przykład 2 (w kompilatorze)
int main(void)
{
  
  int a = 5;
  int b = 15;
  int c = 3;

  //odpowiednik wywołania funkcji obliczenia() oraz użycia y do wydrukowania
  {
    int y;
    y = a*b;
    y = y + c;
    printf( "a*b + c = %d \n", y );
  }

  //zmieniamy wartość argumentów
  a = 80;

  //odpowiednik wywołania funkcji obliczenia() oraz użycia y do wydrukowania
  {
    int y;
    y = a*b;
    y = y + c;
    printf( "a*b + c = %d \n", y );
  }
  
  //zmieniamy wartość argumentów
  a = 97;
  c = 19;

  //odpowiednik wywołania funkcji obliczenia() oraz użycia y do wydrukowania
  {
    int y;
    y = a*b;
    y = y + c;
    printf( "a*b + c = %d \n", y );
  }
   
  return 0;  //zakończ wykonywanie programu
}

Zaskoczeniem dla Ciebie może być użycie w kodzie C przykładu 2 nawiasów klamrowych { ... } oznaczających początek i koniec bloku. Chodzi o to, że wstawiając kod funkcji inline kompilator musiał wyodrębnić zmienną y, którą definiujemy w funkcji. Więcej na ten temat: Zmienne lokalne blokowe

W rzeczywistości kompilator w zależności od parametrów optymalizacji i wielu innych czynników, może przekształcić przykład 1 w nieco inny niż przykład 2, ale z punktu widzenia programisty efekt końcowy działania programu, będzie taki sam.



Funkcja inline vs makrodefinicje

Podobną funkcjonalność realizuje się poprzez makrodefinicje (#define ...). Warto znać różnice pomiędzy tymi metodami:
  • funkcje inline traktowane pod względem kontroli zgodności typów (zarówno argumentów jak i zwracanej danej) są traktowane przez kompilator jak zwykłe funkcje, przez co nie popełnisz błędów trudnych do wykrycia.
  • funkcje inline są znacznie czytelniejsze w kodzie niż makrodefinicje,
  • argumentami funkcji inline mogą być zmienne, a makrodefinicji nie.


Kurs języka C: Spis treści

34
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs C: Funkcje - przykłady


Autor: Dondu

Kurs języka C: Spis treści


Poniżej znajdziesz przykłady definiowania i używania funkcji, które pomogą Ci zrozumieć zasady, które poznałeś w temacie Funkcje - definiowanie.

Zaczniemy jednakże od specyficznej funkcji, którą jest funkcja main( ) ponieważ będzie nam potrzebna w każdym przykładzie:

main()

To funkcja, od której wywołania (uruchomienia) zaczyna się wykonywanie napisanego programu.

W trakcie kompilowania, kompilator szuka funkcji main( ) i przygotowuje kod wynikowy w taki sposób, by realizacja programu rozpoczęła się od tej funkcji. Jeżeli nie zdefiniujesz funkcji main(), to kompilator pokaże błąd kompilacji i przerwie ją.

Ponieważ funkcja main() jest wyjątkiem podlega więc szczególnym zasadom i powinna być zawsze definiowana w następujący sposób:
  • musi być typu int,
  • nie może mieć argumentów,
  • powinna kończyć się zwróceniem zera (od tej zasady są wyjątki).

Prawidłowo napisana definicja funkcji main( ) wygląda więc tak:

Przykład 1 (w kompilatorze)
int main(void){

   return 0;
}

W kompilatorze CManiak znajdziesz ten przykład. Skompiluj w wersji takiej jak jest, by sprawdzić jakie kompilator pokaże komunikaty.




Teraz zdefiniujemy sobie różne funkcje, którym wyznaczymy jakieś zadania do zrealizowania. Aby zobaczyć rezultaty posługiwać się będziemy funkcją drukowania, którą możesz poznać w artykule: printf().


Funkcja bez argumentów i bez zwracania danej

Przykładem będzie wywołanie funkcji pokaz( ) w celu wydrukowania napisu w niej zawartego.

Przykład 2 (w kompilatorze)
void pokaz(void){
  printf("a ten drugi.");
}


int main(void)
{
  printf("Ten napis jest pierwszy, \n");
  pokaz();
  return 0;
}

Program rozpocznie się od wykonania funkcji main( ), która najpierw wydrukuje napis pierwszy, a później wywoła funkcję pokaz( ), która wydrukuje napis drugi.

Zauważ, że w przypadku funkcji pokaz( ) jako typ oraz argument funkcji podaliśmy void, czyli:
  • funkcja nie będzie niczego zwracać,
  • nie przekazujemy funkcji żadnych danych.

Skompiluj powyższy przykład w kompilatorze CManiak i zobacz rezultaty w zakładce Terminal.




Funkcja z argumentem, ale bez zwracania danej

Teraz przekażemy funkcji jakąś daną, a funkcja będzie miała za zadanie ją wydrukować. Do zapamiętania danej w funkcji pokaz( ) użyjemy zmiennej, którą nazwiemy liczba. Przyjmijmy, że dana ta będzie typu char.


Przykład 3 (w kompilatorze)
void pokaz(char liczba){
  printf("%d", liczba);
}

int main(void)
{
  printf("Ilość krów w oborze = ");
  pokaz(5);
  return 0;
}

Ponieważ funkcja nie będzie niczego zwracała stąd typ funkcji ustawiamy na void.

Program główny zawarty w funkcji main( ) wywoła funkcję pokaz( ) podając jako argument liczbę 5. W ten sposób przekazuje liczbę o wartości 5 do funkcji pokaz( ), która "odbiera" tę liczbę i przypisuje zmiennej lokalnej o nazwie liczba.

Ponownie kompilujemy w CManiak'u i oglądamy rezultat w zakładce Terminal. Możesz oczywiście zmieniać wartości przekazywane w zakresie liczb jakie są możliwe dla typu char, gdyż taki typ nadaliśmy zmiennej liczba.




Funkcja bez argumentów, ale zwracająca daną

Czas na funkcję, która zwróci nam jakąś wartość. Na razie funkcji nie podamy żadnego argumentu. Chcemy żeby po prostu zwróciła nam liczbę np. 53.

Przykład 4 (w kompilatorze)
char podaj_liczbe(void){
  return 53;
}

int main(void)
{
  char wynik;
  wynik = podaj_liczbe();
  printf("%d", wynik);
  return 0;
}

W funkcji main( ) zdefiniowaliśmy sobie zmienną wynik, której przypisujemy wartość zwróconą przez funkcję podaj_liczbe( ), a następnie ją wydrukujemy instrukcją printf( ).

Puszczamy w ruch CManiak'a i obserwujemy rezultat działania programu.




Funkcja z argumentami i zwracająca daną

Teraz zbudujemy funkcję z dwoma argumentami (jeden typu char, drugi typu int) oraz oczekiwać będziemy, że funkcja zwróci liczbę typu int.

W funkcji wykonamy mnożenie obu argumentów, a wynik zwrócimy do programu ją wywołującego, czyli w tym wypadku do funkcji main( ).

Przykład 5 (w kompilatorze)
int oblicz(char zm1, int zm2){
  return zm1 * zm2;
}

int main(void)
{
  printf("zm1 * zm2 = %d", oblicz(123, 456) );
  return 0;
}

W porównaniu do przykładu 4, tutaj nie będziemy definiować dodatkowej zmiennej wynik, tylko od razu wykorzystamy zwracaną przez funkcję oblicz( ) wartość do jej wydrukowania funkcją printf( ).

Śmiało testuj w CManiak'u!






Przykłady z życia wzięte


Aby poćwiczyć wykorzystanie funkcji zdefiniujemy funkcję, która będzie odpowiedzialna za obliczanie wyniku funkcji kwadratowej (matematyka) dla podanych liczb jako argumentów wywołania funkcji.


W języku C nie ma operatora arytmetycznego podnoszącego liczbę do kwadratu, więc po prostu pomnożymy x przez siebie, czyli:


Nasza funkcja wymagać będzie podania argumentów: x, a, b oraz c.
Funkcja będzie zwracać wynik obliczeń, czyli y.

Abyśmy mogli wypróbować w kompilatorze jak liczy nasza funkcja na liczbach zmiennoprzecinkowych, wszystkie zmienne oraz funkcja będą typu double.

Przykład 6 (w kompilatorze)
double FunkcjaKwadratowa(double x, double a, double b, double c )
{
  double y;
  y = a*x*x + b*x + c;
  return y;
}

int main(void)
{
  printf("y = %f \n", FunkcjaKwadratowa(102, 0.5, 10, 100) );
  return 0;
}

W kompilatorze CManiak czeka na Ciebie przykład wraz z komentarzami. Wypróbuj jak działa tak napisany program i wstawiaj swoje liczby. Pamiętaj, że przecinek oddziela argumenty, a kropka oddziela część całkowitą liczby od części ułamkowej.




Drugi przykład z życia wzięty, to liczenie silnii danej liczby naturalnej.


W algorytmie wykorzystamy pętlę for(...) { ... }.

Przykład 7 (w kompilatorze)
int silnia(int n)
{
  int k;
  int wynik = 1;
  for(k=1; k<=n; k++){   
     wynik = wynik * k;
  }
  return wynik;
}

int main(void)
{
  printf("silnia = %d \n", silnia(5) );
  return 0;
}

Zaglądając do tego przykładu w CManiak'u możesz poćwiczyć, by zrozumieć jak działa powyższy program wykorzystujący zdefiniowaną funkcję silnia( ).





Sukces vs niepowodzenie

Pisząc swoje programy powinieneś używać pewnego standardu określania, czy działanie funkcji zostało zakończone sukcesem czy niepowodzeniem.

Chodzi o takie funkcje które nie zwracają wyniku jako takiego, ale wykonują jakieś czynności i chcą powiadomić program który je wywołał, że wykonały zadanie z sukcesem lub niepowodzeniem.

Sukces powinieneś definiować jako 1, a niepowodzenie jako 0.

Najprościej zrozumieć to jest poprzez przykład.


Przykład 8 (w kompilatorze)
char CzyWiekszaOdTysiaca(double liczba)
{
  if(liczba > 1000){
    return 1;    //sukces (bo jest większa od 1000)
  }else{
    return 0;    //niepowodzenie
  }
}

int main(void)
{
  printf("%d", CzyWiekszaOdTysiaca(999) );
  return 0;
}

Takie podejście definiowania sukcesu i niepowodzenia pozwala na łatwe wykorzystanie podczas sprawdzania warunków instrukcjami warunkowymi.

I znowu skorzystamy z CManiaka by zobaczyć jak to ułatwia pisanie kodu. Wykorzystamy funkcję CzyWiekszaOdTysiaca() zdefiniowaną w poprzednim przykładzie, ale wynik jej działania wykorzystamy tak:

Przykład 9 (w kompilatorze)
int main(void)
{
  int a;
  for(a=997; a<=1010; a++){

    if( CzyWiekszaOdTysiaca(a) ){
      printf("%d jest większe od 1000 - sukces!!!\n", a);
    }else{
      printf("%d jest mniejsze bądź równe 1000 - niepowodzenie \n", a);
    }
  }
  return 0;
}





Podsumowanie

Funkcje są bardzo elastycznymi fragmentami programu, które mogą spełniać przeróżne zachcianki programisty. Bardzo ważne jest by opanować pisanie funkcji do perfekcji, bo znacząco ułatwi Ci to dzielenie programu na spójne fragmenty, a później składanie z nich całego programu. W ten sposób programy stają się krótkie, czytelne i co najważniejsze łatwo i szybko można je modyfikować.

Co więcej, dobrze napisane funkcje można często wykorzystywać w innych programach, co znacząco przyspiesz ich pisanie.

Jeżeli czegoś nie rozumiesz, śmiało zadawaj pytania w komentarzach.


Kurs języka C: Spis treści

33
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Słowa kluczowe


Autor: Dondu

Kurs języka C: Spis treści

Jak w każdym języku programowania, także w języku C są pewne słowa, które są zarezerwowane dla składni języka i których nie można używać inaczej niż zostały do tego przeznaczone.


Wykaz słów kluczowych:

Słowo Link do artukułu
auto
break do{ ... } while( ... ), for(...) { ... }, while( ... ) { ... }, pętle nieskończone,
switch(...) case ...
case switch(...) case ...
char Typy danych
const Stałe
continue do{ ... } while( ... ), for(...) { ... }, while( ... ) { ... }, pętle nieskończone
default switch(...) case ...
do do{ ... } while( ... ), pętle nieskończone
double Typy danych
else if(...) ... else ...
enum
extern
float Typy danych
for for(...) { ... }
goto goto ...
if if(...) ... else ...
int Typy danych
long Typy danych
register
return Funkcje - definiowanie
short Typy danych
signed Typy danych
sizeof
static
struct
switch switch(...) case ...
typedef
union
unsigned Typy danych
void Funkcje - definiowanie
volatile
while do{ ... } while( ... ), while( ... ) { ... }, pętle nieskończone


Kurs języka C: Spis treści

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs języka C: Funkcje - definicja


Autor: Dondu

Kurs języka C: Spis treści


Funkcje są podstawą języka C!

Są to wydzielone części programu, które wykonują zadane czynności przetwarzając podane im opcjonalnie dane tzw. argumenty. Funkcje po zakończeniu działania mogą (lecz nie muszą) zwracać jakieś dane.

Nawiązując do innych języków funkcje są odpowiednikami podprogramów, czy procedur.

Innymi słowy, to pracownicy specjaliści do określonych małych lub większych zadań. Zaletą funkcji jest możliwość umieszczenia w niej określonej (np. często wykorzystywanej) części kodu, a następnie wywoływanie (uruchamianie) funkcji w wielu różnych miejscach programu.

Z punktu widzenia programisty prawie każdy program należy dzielić na mniejsze zadania realizowane w postaci funkcji. W ten sposób program staje się krótki i czytelny.

Funkcje są bardzo elastyczne, czyli można je definiować i używać na wiele sposobów. Więcej na ten temat znajdziesz w pozostałych tematach rozdziału o funkcjach.



Definiowanie funkcji

Aby funkcję "powołać do życia" należy ją zdefiniować w ten sposób:


typ NazwaFunkcji (argumenty
{

    ... jakiś kod do wykonania

}

gdzie:



typ

Określa typ danej jaką zwraca funkcja po jej wykonaniu. Typ danej może być dowolnym typem danych opisanych szczegółowo w temacie: Typy danych

Jeżeli funkcja nie ma zwracać danej, zamiast podawania typu użyjesz słowa kluczowego void, o którym piszę w dalszej części tego artykułu.




NazwaFunkcji()

Dowolna nazwa jaką sobie wymyślisz jednakże podlegająca takim samym zasadom jak nazwy zmiennych, o których możesz się dowiedzieć w temacie: Zmienne - nazwy i deklaracja

Do nazwy funkcji należy dodać nawiasy zwykłe - otwierający i zamykający.




argumenty

To dane przekazywane do funkcji przez program ją wywołujący (nadrzędny).
Argumenty są opcjonalne, to znaczy, że mogą, ale nie muszą występować.
Argumenty umieszcza się pomiędzy nawiasami zwykłymi podczas definiowania funkcji.

Przykład najprostszego argumentu jakim jest pojedyncza zmienna, której także musimy podać typ np.:

(int ilosc_koni)

Jeżeli przekazujesz więcej niż jeden argument, należy oddzielić je od siebie przecinkami.

(int ilosc_koni, char licznik_krow, double temperatura)

Tak zdefiniowane i przekazane argumenty są dostępne wewnątrz funkcji jako zmienne o określonych przez Ciebie typach i nazwach.

Jeżeli funkcja nie ma mieć argumentów, zamiast nich użyjesz słowa kluczowego void, o którym piszę w dalszej części tego artykułu.




{ }

Nawiasy (klamrowe inaczej zwane sześciennymi) określające początek i koniec funkcji, czyli blok kodu z którego składa się funkcja. Zasady dotyczące bloków kodu poznasz na przykładzie: Zmienne lokalne blokowe






Oprócz powyższych informacji poznać musisz jeszcze dwa słowa kluczowe void oraz return.


void (ang. pustka, próżnia, pozbawiony czegoś)

To słowo kluczowe oznaczające brak wartości lub po prostu nic. W przypadku funkcji słowo to stosuje się:
  • zamiast typu funkcji, gdy funkcja nie ma zwracać żadnej wartości,
  • zamiast argumentów funkcji, gdy funkcja nie ma mieć żadnych argumentów.
Przykłady znajdziesz w dalszych artykułach w rozdziale o funkcjach.




return;
return JakasDana;

W zależności, którą z powyższych wersji instrukcji return użyjesz, służy ona do (odpowiednio):
  • przerwania wykonywania funkcji,
  • przerwania wykonywania funkcji i zwrócenia danej, o typie określonym w czasie definiowania funkcji.

Return może być umieszczony w dowolnym miejscu funkcji lub na jej końcu. Jeżeli w funkcji nie ma słowa return, to po zrealizowaniu kodu zawartego w funkcji następuje wyjście z niej tak, jakby wykonano return bez podania zwracanej danej.


Przykłady do wszystkich omawianych w tym artykule zagadnień, znajdziesz w pozostałych artykułach z rozdziału o funkcjach. 


Kurs języka C: Spis treści

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs języka C: Funkcje


Autor: Dondu

Kurs języka C: Spis treści

Funkcje są podstawą języka C!
Są to wydzielone części programu, które wykonują zadane czynności i/lub przetwarzając podane im dane tzw. argumenty. Funkcje po zakończeniu działania mogą (lecz nie muszą) zwracać jakieś dane.

Nawiązując do innych języków funkcje są odpowiednikami podprogramów, czy procedur.

Innymi słowy, to pracownicy specjaliści do określonych małych lub większych zadań. Zaletą funkcji jest możliwość umieszczenia w niej określonej często wykorzystywanej części kodu, a następnie wywoływanie (uruchamianie) funkcji w wielu różnych miejscach programu.

Z punktu widzenia programisty prawie każdy program należy dzielić na mniejsze zadania realizowane w postaci funkcji. W ten sposób program staje się krótki i przejrzysty.


Funkcje są bardzo elastyczne czyli można je definiować i używać na wiele sposobów.


Zanim przejdziemy do przykładów musisz poznać sporo istotnych rzeczy.


Definiowanie funkcji

Aby funkcję "powołać do życia" należy ją zdefiniować w ten sposób:


typ NazwaFunkcji (argumenty) {

    ... jakiś kod do wykonania

}

gdzie:

typ
Określa typ danej jaką zwraca funkcja po jej wykonaniu. Typ danej może być dowolnym typem danych opisanych szczegółowo w temacie: Typy danych


NazwaFunkcji()
Dowolna nazwa jaką sobie wymyślisz jednakże podlegająca takim samym zasadom jak nazwy zmiennych, o których możesz się dowiedzieć w temacie: Zmienne - nazwy i deklaracja

Do nazwy funkcji należy dodać nawiasy (zwykłe) otwierający i zamykający.

W nawiasach tych wpisujemy:

argumenty
To dane przekazywane do funkcji przez program ją wywołujący (nadrzędny). Więcej na ten temat w dalszej części artykułu.


{ }
Nawiasy określające początek i koniec funkcji, czyli blok kodu z którego składa się funkcja. Zasady dotyczące bloków kodu poznasz na przykładzie: Zmienne lokalne blokowe


Oprócz powyższych poznać musisz jeszcze dwa słowa kluczowe void oraz return.

void (ang. pustka, próżnia, pozbawiony czegoś)

To słowo kluczowe oznaczające brak wartości lub po prostu nic. W przypadku funkcji słowo to stosuje się jako:
  • typ funkcji - oznacza wtedy że funkcja nie zwraca żadnej wartości,
  • argument funkcji - oznacza, że funkcja nie ma żadnych argumentów.
Przykłady w dalszej części artykułu.


return;
return JakasDana;

Return służy do:
  • przerwania wykonywania funkcji,
  • przerwania wykonywania funkcji i zwrócenia danej, o typie określonym w czasie definiowania funkcji (typ).
Return może być umieszczony w dowolnym miejscu funkcji lub na jej końcu.
Jeżeli w funkcji nie ma słowa return, to po dojściu do końca funkcji następuje wyjście z niej tak jakby wykonano return bez podania zwracanej danej.





Przejdźmy więc do meritum tematu. Zaczniemy jednakże od specyficznej funkcji, którą jest funkcja main() ponieważ będzie nam potrzebna w każdym przykładzie:

main()

To funkcja, od której wywołania (uruchomienia) zaczyna się wykonywanie napisanego programu.

W trakcie kompilowania, kompilator szuka funkcji main() i przygotowuje kod wynikowy w taki sposób, by od realizacja programu rozpoczęła się od tej funkcji. Jeżeli nie zdefiniujesz funkcji main(), to kompilator pokażę błąd kompilacji i przerwie ją.

Ponieważ funkcja main() jest wyjątkiem podlega więc szczególnym zasadom i powinna być zawsze definiowana w następujący sposób:
  • musi być typu int,
  • nie może mieć argumentów,
  • musi kończyć się zwróceniem zera.

Prawidłowo napisana definicja funkcji main() wygląda tak:

Przykład 1 (w kompilatorze)
int main(void){

   return 0;
}

W kompilatorze CManiak znajdziesz ten przykład. Skompiluj w wersji takiej jak jest, by sprawdzić jakie kompilator pokaże komunikaty.




Teraz zdefiniujemy sobie różne funkcje, którym wyznaczymy jakieś zadania do zrealizowania. Aby zobaczyć rezultaty posługiwać się będziemy funkcją drukowania printf().


Funkcja bez argumentów i bez zwracania danej

Przykładem będzie wywołanie funkcji w celu wydrukowania napisu w niej zawartego.

Przykład 2 (w kompilatorze)
void pokaz(void){
  printf("a ten drugi.");
}


int main(void)
{
  printf("Ten napis jest pierwszy, \n");
  pokaz();
  return 0;
}

Program rozpocznie się od wykonania funkcji main( ), która najpierw wydrukuje napis pierwszy, a później wywoła funkcję pokaz( ), która wydrukuje napis drugi.

Zauważ, że w przypadku funkcji pokaz( ) jako typ oraz argument podaliśmy void, czyli:
  • funkcja nie będzie niczego zwracać,
  • nie przekazujemy funkcji żadnych danych.

Skompiluj powyższy przykład w kompilatorze CManiak i zobacz rezultaty w zakładce Terminal.


Funkcja z argumentem, ale bez zwracania danej

Teraz przekażemy funkcji jakąś daną, którą będzie miała wydrukować. Do zapamiętania danej w funkcji pokaz( ) użyjemy zmiennej, którą nazwiemy liczba. Przyjmijmy, że dana ta będzie typu char.


Przykład 3 (w kompilatorze)
void pokaz(char liczba){
  printf("%d", liczba);
}

int main(void)
{
  printf("Ilość krów w oborze = ");
  pokaz(5);
  return 0;  //zakończ wykonywanie programu
}

I ponownie kompilujemy w CManiak'u i oglądamy rezultat w zakładce Terminal.




Funkcja bez argumentów, ale zwracająca daną


Czas na funkcję, która zwróci nam jakąś wartość. Na razie funkcji nie podamy żadnego argumentu.

Przykład 4 (w kompilatorze)
//definiujemy funkcję podaj_liczbe() która:
// - będzie zwracała liczbę typu char
// - nie wymaga podania żadnych argumentów
char podaj_liczbe(void){

  return 53;  //zakończ wykonywanie funkcji zwracając liczbę 53
  
}

//program zaczyna się od funkcji main()
int main(void)
{

  //wydrukuj liczbę którą zwróci funkcja 
  printf("%d", podaj_liczbe() );
  
  return 0;  //zakończ wykonywanie programu
}


Funkcja z argumentami i zwracająca daną

Teraz zbudujemy funkcję z dwoma argumentami (jeden typu char, drugi typu int) oraz oczekiwać będziemy, że funkcja zwróci liczbę typu int.


Przykład 5 (w kompilatorze)
//definiujemy funkcję oblicz() typu int  
//z dwoma argumentami typu char oraz int 
int oblicz(char zm1, int zm2){

  return zm1 * zm2;  //zwróć wynik mnożenia argumentów
  
}

//program zaczyna się od funkcji main()
int main(void)
{
  
  //pokaż napis
  printf("zm1 * zm2 = %d", oblicz(123, 456) );
  
  return 0;  //zakończ wykonywanie programu
}






Przykłady z życia wzięte


Aby poćwiczyć wykorzystanie funkcji zdefiniujemy funkcję, która będzie odpowiedzialna za obliczanie wyniku funkcji kwadratowej (matematyka) dla podanych liczb jako argumentów wywołania funkcji.


W języku C nie ma operatora arytmetycznego podnoszącego liczbę do kwadratu, więc po prostu pomnożymy x przez siebie, czyli:


Nasza funkcja wymagać będzie podania argumentów: x, a, b oraz zwracając wynik obliczenia czyli y.

Abyśmy mogli wypróbować w kompilatorze jak liczy nasza funkcja na liczbach zmiennoprzecinkowych, wszystkie zmienne oraz funkcja będą typu double.

Przykład 6 (w kompilatorze)
double FunkcjaKwadratowa(double x, double a, double b, double c )
{
  double y;
  y = a*x*x + b*x + c;
  return y;  //zwróć wynik
}

int main(void)
{
  printf("y = %f \n", FunkcjaKwadratowa(102, 0.5, 10, 100) );
  return 0;
}

W kompilatorze CManiak czeka na ciebie przykład wraz z komentarzami. Wypróbuj jak działa powyższy przykład i wstawiaj swoje liczby. Pamiętaj, że przecinek oddziela argumenty, a kropka oddziela część całkowitą liczby od części ułamkowej.


Drugi przykład z życia wzięty to liczenie silni danej liczby naturalnej.


W algorytmie wykorzystamy pętlę for(...) { ... }.


Przykład 7 (w kompilatorze)
//definiujemy funkcję silnia() typu int (liczby całkowite)
//z jednym argumentem n typu int
int silnia(int n)
{
  int k;                 //zmienna pomocnicza
  int wynik=1;           //zmienna przechowująca wynik liczenia silnii
  
  //pętla licząca silnię
  for(k=1; k<=n; k++){   
     wynik = wynik * k;
  }
  
  return wynik;  //zwróć wynik
}


//program zaczyna się od funkcji main()
int main(void)
{
  printf("silnia = %d \n", silnia(5) );   //pokaż silnię dla liczby 5
  return 0;  //zakończ wykonywanie programu
}

I znowu zaglądając do tego przykładu w CManiak'u możesz poćwiczyć rozumienie tego przykładu użycia funkcji w języku C.





Sukces vs niepowodzenie

Pisząc swoje programy powinieneś używać pewnego standardu określania, czy działanie funkcji zostało zakończone sukcesem czy niepowodzeniem.

Chodzi o takie funkcje które nie zwracają wyniku jako takiego, ale wykonują jakieś czynności i chcą powiadomić program który je wywołał, że wykonały zadanie z sukcesem lub niepowodzeniem.

Sukces powinieneś definiować jako 1, a niepowodzenie jako 0.

Najprościej zrozumieć to jest poprzez przykład.


Przykład 8 (w kompilatorze)
//definiujemy funkcję CzyWiekszaOdTysiaca() typu char
//z jednym argumentem liczba typu double
char CzyWiekszaOdTysiaca(double liczba)
{
  //sprawdzamy czy liczba jest większa od tysiąca
  if(liczba > 1000){
    return 1;    //sukces (bo jest większa od 1000)
  }else{
    return 0;    //niepowodzenie
  }
}


//program zaczyna się od funkcji main()
int main(void)
{
  //sprawdź liczbę
  printf("%d", CzyWiekszaOdTysiaca(999) );
  return 0;  //zakończ wykonywanie programu
}

Takie podejście definiowania sukcesu i niepowodzenia pozwala na łatwe wykorzystanie podczas sprawdzania warunków instrukcjami warunkowymi.

I znowu skorzystamy z CManiaka by zobaczyć jak to ułatwia pisanie kodu. Wykorzystamy funkcję CzyWiekszaOdTysiaca() zdefiniowaną w poprzednim przykładzie, ale wynik jej działania wykorzystamy tak:

Przykład 9 (w kompilatorze)
//program zaczyna się od funkcji main()
int main(void)
{
  int a;    //deklaracja zmiennej, którą będziemy sprawdzać
  
  //zaczniemy dla a=997 a skończymy na a=1010 
  //zwiększając a o jeden za każdym krokiem pętli for()
  for(a=997; a<=1010; a++){

    //sprawdzamy czy a jest większe od 1000 wywołując 
    //funkcję i podając jako argument zmienną a
    if( CzyWiekszaOdTysiaca(a) ){
      
      //gdy jest sukces
      printf("%d jest większe od 1000 - sukces!!!\n", a);
      
    }else{
      
      //gdy jest niepowodzenie
      printf("%d jest mniejsze bądź równe 1000 - niepowidzenie \n", a);
      
    }
  }
  return 0;  //zakończ wykonywanie programu
}





Podsumowanie

Funkcje są bardzo elastycznymi fragmentami programu, które mogą spełniać przeróżne zachcianki programisty. Bardzo ważne jest by opanować pisanie funkcji do perfekcji, bo znacząco ułatwi Ci to dzielenie programu na spójne fragmenty, a później składanie z nich całego programu. W ten sposób programy stają się krótkie, czytelne i co najważniejsze łatwo i szybko można je modyfikować.

Co więcej, dobrze napisane funkcje można często wykorzystywać w innych programach, co znacząco przyspiesz ich pisanie.

Jeżeli czegoś nie rozumiesz, śmiało zadawaj pytania w komentarzach.

Kurs języka C: Tips & Tricks - Spis treści
Kurs języka C: Spis treści

33
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Komentowanie kodu


Autor: Dondu

Kurs języka C: Spis treści

Każdy kod powinien zawierać komentarze. Im dokładniej opiszesz poszczególne fragmenty kodu, tym łatwiej będzie Ci go pisać (gdy jest duży) lub modyfikować po upływie dłuższego czasu.

Dodatkowo gdy masz problem i pokazujesz komuś swój kod np. na forum, osoba go sprawdzająca będzie mogła szybko zrozumieć jego działanie, co zachęca do pomagania i skraca czas znalezienia błędu.

W języku C możesz wykorzystać dwa rodzaje komentarza:
  • /* ... tutaj komentarz ... */
  • //   ... tutaj komentarz ... 


/* ... tutaj komentarz ... */

To podstawowy rodzaj komentarza. Umożliwia pisanie komentarzy zawierających znaki specjalne jak np. znak nowej linii, czyli umożliwia pisanie komentarza w wielu linijkach. Komentarz rozpoczyna się w miejscu wstawienia znacznika startu komentarza /* i kończy w miejscu umieszczenia znacznika końca */

Innymi słowy cały tekst komentarza musi się zawierać pomiędzy znacznikami początku i końca komentarza.

Przykład 1 (w kompilatorze)
/* Przykład komentarza jednoliniowego */

/* To jest przykład komentarza,
który zawiera się
w wielu linijkach */

/*
   Można także pisać
   bardziej 
   przejrzyście.
*/

Komentarz zawarty pomiędzy znacznikami /* oraz */ jest ignorowany przez kompilator, przez co możesz umieszczać go np. wewnątrz linijek kodu. Jednakże osobiście odradzam takie wykorzystanie komentarzy.

Przykład 2 (w kompilatorze)
a = 2 * /* komentarz */ 3;




//   ... tutaj komentarz ... 

Ten rodzaj komentarza umożliwia umieszczanie komentarza od miejsca umieszczenia znacznika  //  do końca linii. W związku z tym, każda nowa linia komentarza musi zaczynać się od znacznika //


Przykład 3 (w kompilatorze)
//pierwsza linia komentarza
//druga
//trzecia
//itd.

Ten rodzaj komentarza możesz wykorzystać także na końcu linii kodu. Umożliwia to bardzo przejrzyste komentowanie poszczególnych jego fragmentów.

Przykład 4 (w kompilatorze)
 //deklaracja zmiennych
 int a;  //zawiera dane o ilości królików w lesie 
 
 //sprawdzamy ilość królików
 if(a>2){
    a=a-12;  //pomniejsz ilość o dwanaście
 }else{
    a=a+3;   //powiększ ilość o 3
 }



Komentowanie mieszane

Oba rodzaje komentowania można wykorzystywać w sposób pokazany w poniższym przykładzie. Pozwala to na wykorzystanie komentarza /* ... */ do "zamarkowania" bloku nieużywanego kodu, w którym są wykorzystane komentarze //

Jest to w szczególności przydatne w czasie szukania przyczyn nie działania kodu. Możesz w ten sposób szybko sprawdzać różne jego wersje.

Przykład 5 (w kompilatorze)
//deklaracja zmiennych
int a;  //zawiera dane o ilości królików w lesie 
 

/*  wyłączyłem ten fragment kodu na czas szukania błędu
    //sprawdzamy ilość królików
    if(a>2){
       a=a-12;  //pomniejsz ilość o dwanaście
    }else{
       a=a+3;   //powiększ ilość o 3
    }
*/

    
a = a+1;  //wstawiłem tylko na czas szukania błędu


Kolor komentarzy

Gdy w kodzie jest sporo komentarzy, może to utrudnić wzrokowe przeglądanie kodu. Ja radzę sobie z tym poprzez odpowiednie dobranie koloru komentarzy w edytorze kodu. Na przykład w MPLAB Microchipa ustawiłem sobie kolor #A4E3B7, by znacząco zwiększyć czytelność kodu.

Zwiększenie czytelności kodu programu poprzez odpowiedni kolor komentarzy.

Kurs języka C: Spis treści

32
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs języka C: Printf() i funkcje pochodne


Autor: Dondu

Kurs języka C: Spis treści


Funkcje z rodziny printf, formatują tekst podany w argumentach w sposób określony jednym lub wieloma formatami. Tekst ten powstaje z ciągów znaków ASCII oraz liczb dowolnych typów.

Do rodziny tej należą funkcje: printf, fprintf, sprintf, snprintf, vprintf, vfprintf, vsprintf oraz vsnprintf.

Argumenty funkcji wpisuje się według szablonu:

%[flaga][szerokość][.precyzja][format]

Nawiasy kwadratowe oznaczają, iż dany parametr jest opcjonalny, czyli może lecz nie musi występować.




[format]

Ciągi znaków powstające z liczb i znaków ASCII możesz formatować za pomocą dostępnych opcji formatu:

Tabela 1 - Dostępne formaty i ich działanie

FormatDziałanie
%%Znak ASCII: %
sCiąg znaków ASCII.
cPojedynczy znak kodu ASCII.
dLiczba dziesiętna całkowita ze znakiem.
iLiczba dziesiętna całkowita ze znakiem.
eLiczba dziesiętna w zapisie naukowym (mała litera e).
ELiczba dziesiętna w zapisie naukowym (duża litera E).
fLiczba dziesiętna zmiennoprzecinkowa (wraca małe litery w przypadku, gdy wynik jest:: inf, infinity, lub nan).
FLiczba dziesiętna zmiennoprzecinkowa (zwraca duże litery w przypadku, gdy wynik jest:: INF, INFINITY, lub NAN).
gWybiera format e lub f w zależności, który będzie krótszy (mniej znaków).
GWybiera format E lub F w zależności, który będzie krótszy (mniej znaków).
oLiczba ósemkowa całkowita bez znaku.
uLiczba dziesiętna bez znaku.
xLiczba szesnastkowa bez znaku (małe litery).
XLiczba szesnastkowa bez znaku (duże litery).
pWskaźnik.


W kompilatorze CManiak znajdziesz przygotowany przykład nr 1 pokazujący wynik pracy funkcji printf() dla poszczególnych formatów. Porównaj otrzymane wyniki z odpowiadającymi im liniami kodu w kompilatorze:

Przykład 1 (w kompilatorze)
  printf("Znak ASCII: %%");
  printf("Ciąg znaków ASCII: %s",  "jakis tekst");
  printf("Znak ASCII o kodzie 65: %c",  65);
  printf("Liczba dziesiętna całkowita ze znakiem: %d",  -65);
  printf("Liczba dziesiętna całkowita ze znakiem: %i",  -65);
  printf("Liczba dziesiętna w zapisie naukowym (małe e): %e", -165.1235);
  printf("Liczba dziesiętna w zapisie naukowym (duże E): %E", -165.1235);
  printf("Liczba dziesiętna zmiennoprzecinkowa: %f",  -165.1235);
  printf("Liczba dziesiętna zmiennoprzecinkowa: %F",  -165.1235);
  printf("W zależności co krótsze %%e czy %%f : %g",  12213365.133235);
  printf("W zależności co krótsze %%e czy %%f : %g",  12215.133);
  printf("W zależności co krótsze %%E czy %%F : %G",  12213365.133235);
  printf("W zależności co krótsze %%E czy %%F : %G",  12215.133);
  printf("Liczba ósemkowa całkowita bez znaku: %o",  64);
  printf("Liczba dziesiętna całkowita bez znaku: %u",  65);
  printf("Liczba szesnastkowa bez znaku (małe litery) %x",  346874);
  printf("Liczba szesnastkowa bez znaku (duże litery) %X",  346874);
  
  int a=1234;    //deklaracja zmiennej o wartości 1234
  printf("Wskaźnik (adres zmiennej a): %p", &a); //pokaż wskaźnik



[szerokość]

Za pomocą tego parametru ustala się jaką szerokość liczoną w ilości znaków ASCII ma mieć wynik działania funkcji.

Jeżeli wynik ma mniej znaków niż określa to parametr [szerokość], to wyrównywany jest przez dodanie spacji z lewej strony. Innymi słowy możesz w ten sposób wyrównywać liczby do prawej strony.

Na przykład jeżeli chcesz wyświetlić liczbę za pomocą formatu d o szerokości pola wynoszącej 6 cyfr, możesz zrobić tak:
%6d

Podobnie jest w przypadku ciągów znaków ASCII. Ponieważ do ciągów znaków stosujemy wg powyższej tabeli format s, stąd dla szerokości pola o 6 znakach, parametr ten zapiszemy podobnie jak w przypadku liczb:
%6s

Zobacz w kompilatorze CManiak jaki efekt otrzymasz uruchamiając przykład nr 2:

Przykład 2 (w kompilatorze)
  printf("%6d", 1235);
  printf("%6d", 78);
  printf("%6d", 6);
  printf("%6d", 455897);

  char tekst[]="abcd";   //ciąg znaków w zmiennej tekst
  printf("%6s", tekst);




[.precyzja]

Parametr ten pozwala ustalić jaką precyzję czyli ilość cyfr, które mają zostać pokazane. Liczba określająca precyzję musi być poprzedzona kropką.

Precyzja dla poszczególnych formatów ma różne znaczenie:

Tabela 2 - Parametr precyzji dla poszczególnych formatów:

dla formatówdziałanie
d, i, o, u, x, Xminimalna liczba cyfr, które mają być wyświetlone (domyślnie 1)
a, A, e, E, f, Fliczba cyfr, które mają być wyświetlone po kropce (domyślnie 6)
g, Gliczba cyfr znaczących (domyślnie 1)
smaksymalna liczba znaków


Poniższy przykład pokazuje po jednym przypadku z tabeli 2. Zobacz jak wykona przykład nr 3 kompilator CManiak.

Przykład 3 (w kompilatorze)
  printf("%.3d", 5);  
  printf("%.3f", 5397.9876); 
  printf("%.3g", 5397.9876); 
  printf("%.3s", "CManiak jest super!"); 




[szerokość.precyzja]

Teraz połączmy parametr szerokości i precyzji. Otrzymamy w ten sposób liczby i teksty o odpowiedniej precyzji wyrównane do prawej.

Zobacz efekt uruchomienia kodu przykładu 4 w kompilatorze CManiak.

Przykład 4 (w kompilatorze)
  printf("%12.3d", 5);  
  printf("%12.3f", 5397.9876); 
  printf("%12.3g", 5397.9876); 
  printf("%12.3s", "CManiak jest super!"); 




[flaga]

Parametr flaga może dodatkowo modyfikować znaczenie pozostałych parametrów.
Uwaga! Flaga ma nadrzędne znaczenie przez co czasami zmienia działanie innych parametrów.

Tabela 3 - Flagi i ich znaczenie:

Flagadziałanie
- (minus)wymuszaj wyrównanie do lewej
+ (plus)liczby zawsze mają być poprzedzone znakiem (+ lub -)
spacjaliczby nieujemne mają być poprzedzone spacją
# (hash)dla formatu o (liczba ósemkowa) wymusza zero na początku.
dla formatów x lub X (liczba szesnastkowa) wymusza dodanie na początku
odpowiednio 0x lub 0X
dla formatów a, A, e, E, f, F, g, G wymusza kropkę nawet wtedy,
gdy nie ma żadnych cyfr części ułamkowej.
dla formatów g lub G końcowe zera nie są usuwane
0 (zero)Dla formatów: d, i, o, u, x, X, a, A, e, E, f, F, g, G
wymusza uzupełnienie wyrównania zerami zamiast spacjami.
Dla formatów: d, i, o, u, x, X jeżeli określona jest precyzja, to flaga ta jest ignorowana.

Poniżej przykłady użycia flag.


Flaga: - (minus)

Przykład oprzemy o pole szerokości 12 znaków, precyzji 3 znaków dla liczby całkowitej dziesiętnej równej 5. Uruchamiając przykład w kompilatorze CManiak zobaczysz jak realizowane jest wymuszenie flagą - (minus) wyrównania do lewej pomimo, że określamy szerokość pola na 12 znaków, co powinno było wyrównać liczbę do prawej.

Przykład 5 (w kompilatorze)
  printf("%-12.3d\n", 5);  
  printf("%12.3d", 5);  



Flaga: + (plus)

Prosty przykład dla liczb całkowitych dziesiętnych dodatniej i ujemnej z użyciem flagi + (plus). Znak zawsze jest dodawany, niezależnie, czy jest on plusem,, czy minusem. Sprawdź uruchamiając przykład w kompilatorze CManiak.

Przykład 6 (w kompilatorze)
  printf("%+d", 0); 
  printf("%+d", 5317); 
  printf("%+d", -5317); 



Flaga: spacja


Poniżej przykład dla liczb całkowitych dziesiętnych dodatniej i ujemnej z użyciem flagi + (plus). Dla liczb nieujemnych dodana zostanie spacja z przodu. Sprawdź uruchamiając przykład w kompilatorze CManiak.

Przykład 7 (w kompilatorze)
  printf("% d", 0); 
  printf("% d", 5317); 
  printf("% d", -5317); 



Flaga: 0 (zero)


Przykład pokazuje użycie flagi 0 (zero) dla liczby całkowitej dziesiętnej przy żądaniu, by pole miało szerokości 12 znaków. Rezultat możesz sprawdzić uruchamiając ten przykład w kompilatorze CManiak.

Przykład 8 (w kompilatorze)
  printf("%012d", 0); 
  printf("%012d", 5317); 
  printf("%012d", -5317); 




Flaga: # dla formatu o


Format o dotyczy liczb ósemkowych. Przygotowałem przykład dla liczby ósemkowej 100 z użyciem flagi # (hash) oraz bez niej. W kompilatorze CManiak zobaczysz jak dodawane jest zero na początku liczby.

Przykład 9 (w kompilatorze)
  printf("%#o", 64);   //64 dziesiętnie to w kodzie ósemkowym 100
  printf("%o", 64);




Flaga: # dla formatów: x, X


Formaty x oraz X dotyczą liczb szesnastkowych. Zobacz więc jak flaga # w połączeniu z tymi formatami powoduje dodanie na początku liczby przedrostków odpowiednio 0x lub 0X. W CManiak'u także dostępny jest ten przykład.


Przykład 10 (w kompilatorze)
  printf("%#x", 32498); //32498 dziesiętnie, to w kodzie szesnastkowym 7ef2
  printf("%x", 32498);



Flaga: # dla formatów: 
a, A, e, E, f, F, g, G

Dla tych formatów (liczby zmiennoprzecinkowe) flaga # wymusza kropkę nawet wtedy, gdy nie ma żadnych cyfr części ułamkowej. I znowu CManiak zaprezentuje Ci działanie tej flagi.


Przykład 11 (w kompilatorze)
  printf("%#12.0f", 789.0);
  printf("%12.0f", 789.0);




Flaga: # dla formatów: 
g, G

Dla tych formatów flaga # wymusza pozostawienie końcowych zer. CManiak czeka gotowy do pracy :-)


Przykład 12 (w kompilatorze)
  printf("%#g", 789.0);
  printf("%g", 789.0);


Kurs języka C: Spis treści

29
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Instrukcja: goto


Autor: Dondu

Kurs języka C: Spis treści

Instrukcja goto dla początkującego w C, jest jak dziadek chłopa Macieja dla jego rodziny (oglądaj od 6 min 25 sek):




Dlatego świadomie i celowo nie opisuję jej w niniejszym kursie, a Ty jeżeli nie jesteś profesjonalnym programistą C z wieloletnim stażem, to lepiej oglądnij cały skecz, zamiast tracić czas na szukanie przyczyn nie działania Twojego programu po zastosowaniu instrukcji goto.

W szczególności zapomnij o goto, jeżeli wcześniej programowałeś 
w językach Basic'o podobnych np. BASCOM.
Od dzisiaj dla Ciebie
GOTO nie istnieje!


Kurs języka C: Spis treści

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Kurs języka C: Pętle nieskończone


Autor: Dondu

Kurs języka C: Spis treści

Pętle nieskończone to takie pętle, których warunek kontynuowania jest ciągle spełniony. Każdy rodzaj pętli oferowanych przez język, może być pętlą nieskończoną.

Pętle takie są często wykorzystywane w programach mikrokontrolerów, jako pętle główne.

Aby pętla była nieskończoną wystarczy odpowiednio ustawić jej argumenty:
 while (1) 
 do { } while (1);
 for (;;)

Poniżej trzy przykłady dostępne do uruchomienia w kompilatorze CManiak.

Ponieważ są to pętle nieskończone, stąd w czasie wykonywania programu na serwerze kompilatora CManiak zostanie uruchomione zabezpieczenie w postaci maksymalnego czasu wykonywania programu. Czas ten ustawiony jest na 5 sekund. Dlatego po 5 sekundach program zostanie przerwany, a kompilator pokaże informację o tym fakcie:

Error: Wykonanie programu zakończone niepowodzeniem.
Przekroczono czas wykonywania programu (maks: 5 sekund).

Normalnie oczywiście tego zabezpieczenia nie ma, więc pętle wykonywałyby się cały czas.

Przykład 1 (w kompilatorze)
  while(1)
  {
    //tutaj kod
  }

Przykład 2 (w kompilatorze)
  do
  {
    //tutaj kod
  } while(1);

Przykład 3 (w kompilatorze)
  for(;;)
  {
    
    //tutaj kod
    
  }



Break

Czasami zachodzi potrzeba przerwania wykonywania pętli pomimo, że warunek jest nadal spełniony. Wystarczy w pętli użyć instrukcji break, by pętla natychmiast została przerwana. Szczegóły znajdziesz w tematach dot. poszczególnych pętli.


Kurs języka C: Spis treści

28
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Działy
Działy dodatkowe
Inne
O blogu




Dzisiaj
--> za darmo!!! <--
1. USBasp
2. microBOARD M8


Napisz artykuł
--> i wygraj nagrodę. <--


Co nowego na blogu?
Śledź naszego Facebook-a



Co nowego na blogu?
Śledź nas na Google+

/* 20140911 Wyłączona prawa kolumna */
  • 00

    dni

  • 00

    godzin

  • :
  • 00

    minut

  • :
  • 00

    sekund

Nie czekaj do ostatniego dnia!
Jakość opisu projektu także jest istotna (pkt 9.2 regulaminu).

Sponsorzy:

Zapamiętaj ten artykuł w moim prywatnym spisie treści.