|
Wyjście Spis treści Wstecz Dalej
Autor artykułu |
©2026 mgr Jerzy Wałaszek
|
Jeśli dotarłeś do tego miejsca i przyswoiłeś sobie przedstawiony materiał, to możesz rozpocząć naukę programowania mikrokontrolerów w języku C. Niniejszy kurs miał na celu zaznajomienie cię z podstawami języka C. Język ten jest bardzo potężny i pozwala wygodnie programować komputery zarówno duże (np. superkomputer Cray) jak i całkiem małe, jakimi są niewątpliwie mikrokontrolery. Kurs ten nie jest kompletny, jeśli chodzi o język C. Lecz takie było założenie. Nie musisz znać wszystkich tajników, lecz tylko te, które będą ci potrzebne (odnosi się to wszystkich kursów w tym artykule). A ten cel kurs spełnia.
W tym rozdziale postaram się uzupełnić twoją wiedzę o języku C. Jeśli czegoś nie rozumiesz, nie przejmuj się. Umiejętności programowania przychodzą z czasem, po napisaniu dziesiątek/setek/tysięcy programów. Pierwsze "dzieła" są zawsze ułomne. Później nabierzesz doświadczenia i twoje programy staną się bardziej profesjonalne. Najszybciej nauczysz się programować przy tworzeniu różnych projektów z mikrokontrolerami.
Podsumujmy typy danych, które możesz napotkać w języku C:
Aby uniknąć niejednoznaczności, można stosować modyfikatory typu:
| signed char – znaki o kodach od -128
do 127 unsigned char – znaki o kodach od 0 do 255 |
W świecie mikrokontrolerów 8-bitowych typ int najczęściej oznacza daną o długości 16 bitów. Zakres wynosi wtedy od -32768 do 32767 (-215...215-1). Jednakże może również oznaczać daną o długości 1 bajta, ponieważ to jest naturalna dana całkowita dla mikroprocesora 8-bitowego. Zwykle taki tryb wymaga włączenia w kompilatorze odpowiedniej opcji. Musisz to sobie sprawdzić w dokumentacji środowiska programowania, którego będziesz używał.
Typ int może występować z modyfikatorami (przy stosowaniu modyfikatorów nazwę int można pominąć):
| unsigned int | – | liczby całkowite bez znaku. Zakres zależy od rozmiaru int: 4-bajty – 0...4294967296 (0...232-1) ; 2-bajty – 0...65535 (0...216-1) ; 1-bajt – 0...255 (0...28-1). |
| short int | – | oznacza tzw. krótką liczbę całkowitą, która nie może posiadać więcej bitów od typu int. Jeśli typ int jest 32-bitowy, to typ short jest zwykle 16-bitowy. Jeśli typ int jest 16-bitowy, to typ short może być 16-bitowy lub 8-bitowy (należy sprawdzić z konkretnym kompilatorem). |
| unsigned short int | – | krótka liczba całkowita bez znaku. Rozmiar taki sam jak dla short. |
| long int | – | długa liczba całkowita. Zwykle rozmiar jest dwa razy dłuższy od rozmiaru int, lecz np. w Code::Blocks long int oznacza dokładnie to samo, co int, czyli liczbę 4-bajtową. Jeśli typ int oznacza liczby 2-bajtowe, to typ long int będzie oznaczał liczby 4-bajtowe. Jeśli typ int oznacza liczby 1-bajtowe, to typ long int będzie oznaczał liczby 2-bajtowe. |
| unsigned long int | – | długa liczba całkowita bez znaku. Rozmiar taki sam jak dla long int. |
| long long int | – | bardzo długa liczba całkowita. Zwykle ma rozmiar cztery razy dłuższy od rozmiaru int. W Code::Blocks typ long long int oznacza daną 64-bitową (8 bajtów) o zakresie od -9223372036854775808 do 9223372036854775807 (-263 ... 263-1). Typ long long int może nie być dostępny na danej platformie (np. PIC). Jeśli typ int jest 16-bitowy, to typ long long int jest 64-bitowy. Jeśli typ int jest 8-bitowy, to typ long long int jest 32-bitowy. |
| unsigned long long int | – | bardzo długa liczba całkowita bez
znaku. Długość taka sama jak dla long long int. Dla 8-bajtów zakres wynosi od 0...18446744073709551615 (0...264-1). |
W języku C możesz definiować własne typy danych za pomocą słowa kluczowego typedef:
typedef nazwa_typu_istniejącego nowa_nazwa; |
Na przykład: nie podoba ci się nazwa typu unsigned int. Nie ma sprawy:
/*
Własne typy danych
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 26.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
// Definicja własnego typu
typedef unsigned int bezznakowe;
int main()
{
// definiujemy zmienne z nową nazwą typu
bezznakowe a,b,c;
setlocale(LC_ALL,"");
a = 1; b = a + 10; c = 12 * b;
printf("%u %u %u\n", a, b, c);
return 0;
}
|
W ten sposób da się pozmieniać nazwy wszystkich typów podstawowych, tylko po co? Lepszym zastosowaniem dla typedef jest tworzenie zupełnie nowych typów danych:
/*
Własne typy danych
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 26.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
// Definicja własnego typu
typedef struct
{
float re,im;
} complex;
int main()
{
// definiujemy zmienne z nową nazwą typu
complex a = {1,2};
complex b = {2,2};
complex c;
setlocale(LC_ALL,"");
c.re = a.re + b.re;
c.im = a.im + b.im;
printf("DODAWANIE LICZB ZESPOLONYCH\n"
"---------------------------\n\n"
"Liczba a = %f + j%f\n"
"Liczba b = %f + j%f\n"
"Suma a + b = %f + j%f\n",
a.re,a.im,b.re,b.im,c.re,c.im);
return 0;
}
|
W programie utworzyliśmy bezimienną strukturę i przypisaliśmy
jej typ do nazwy nowego typu danych complex
(liczba zespolona). Struktura zawiera dwa pola:
re
– część rzeczywista liczby zespolonej (ang.
real part) i im – część urojona
(ang. imaginary part). Wg nowego typu complex
zdefiniowaliśmy 3 zmienne: a
Jeśli nie wiesz, czym są liczby zespolone, to dowiesz się tego w tym rozdziale.
Bardzo przydatny jest tzw. typ wyliczeniowy (an. enumerated type). Tworzymy go za pomocą słówka kluczowego enum w sposób następujący:
enum nazwa_typu {nazwa_1, nazwa_2,...,nazwa_n};
|
| nazwa_typu | – | określa nazwę dla definiowanego typu wyliczeniowego. |
| nazwa_i | – | wewnątrz klamerek podajemy ciąg
nazw dla stałych, którym zostaną nadane kolejne wartości całkowite począwszy od zera w górę. |
Jak to działa? Weźmy prosty przykład:
enum weekday {sunday, monday, tuesday, wednesday, thursday, friday, saturday};
|
W efekcie tej definicji powstanie nowy typ danych o nazwie enum weekday (dzień tygodnia). Oprócz nazwy typu powstaje zbiór stałych o nazwach dni tygodnia. Stałe te otrzymują kolejne wartości:
| sunday ← 0 monday ← 1 tuesday ← 2 wednesday ← 3 thursday ← 4 friday ← 5 saturday ← 6 |
Teraz na podstawie nowego typu możesz definiować zmienne i nadawać im wartości określone przez stałe:
enum weekday a,b,c; ... a = tuesday; ... if(b == c && b == friday) ... ... |
Czy naprawdę potrzebujemy typ enum? Oczywiście, że nie. Bez niego moglibyśmy się obejść, lecz dobrze go mieć, bo jest czasem wygodny. Nasze stałe moglibyśmy zdefiniować następująco:
#define sunday 0 #define monday 1 #define tuesday 2 #define wednesday 3 #define thursday 4 #define friday 5 #define saturday 6 ... int a,b,c; ... a = wednesday; ... if(b == c && b == saturday) ... ... |
A teraz wyobraź sobie, że zrobiłeś drobną pomyłkę:
#define sunday 0 #define monday 1 #define tuesday 2 #define wednesday 3 #define thursday 4 #define friday 5 #define saturday 5 ... int a,b,c; ... a = wednesday; ... if(b == c && b == saturday) ... ... |
I już program staje się wadliwy. Typ enum nadaje stałym kolejne wartości automatycznie. Dodatkowo zadanie wykonywane jest przez kompilator, a nie przez preprocesor. Zatem można już w trakcie kompilacji wyłapać różne błędy.
Wartości stałych w typie enum możesz też sam okreslić:
enum weekday {sunday,
monday,
tuesday = 6,
wednesday,
thursday,
friday,
saturday};
|
Jeśli na liście stałych nadasz jednej z nich konkretną wartość, to wszystkie stałe, które na liście występują później, otrzymają wartości kolejne od nadanej. W tym przypadku otrzymasz:
| sunday ← 0 monday ← 1 tuesday ← 6 wednesday ← 7 thursday ← 8 friday ← 9 saturday ← 10 |
W krańcowym przypadku możesz sam określić wartość każdej stałej:
enum weekday {sunday = 7,
monday = 2,
tuesday = 6,
wednesday = 5,
thursday = 99,
friday = 12,
saturday = 87};
|
Zmienna zbudowana na podstawie zdefiniowanego typu wyliczeniowego jest zwykle typu int. Jednak nie licz na to zbytnio, ponieważ w definicji typu wyliczeniowego określono jedynie, że zmienna wynikowa będzie takiego typu, który pomieści wszystkie wartości stałych.
/*
Typ wyliczeniowy
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 30.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
enum boolean {False,True};
int main()
{
setlocale(LC_ALL,"");
printf("%d\n",sizeof(enum boolean));
return 0;
}
|
Również kompilator nie kontroluje wartości nadawanym zmiennym typu enum:
/*
Typ wyliczeniowy
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 30.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
enum boolean {False,True};
int main()
{
enum boolean a,b,c;
setlocale(LC_ALL,"");
a = False; // OK
b = True; // OK
c = 3; // ??? też OK!
printf("%d %d %d\n", a, b, c);
return 0;
}
|
Stała jest wartością, która nie zmienia się w trakcie wykonywania programu. Stałe możemy tworzyć na kilka sposobów:
#define nazwa wartość
Jest to dyrektywa preprocesora, która przyporządkowuje nazwie pewną stałą wartość. Dzięki niej możemy łatwo zdefiniować w programie wartości logiczne:
#define FALSE 0 #define TRUE 1 |
Od tego momentu każde wystąpienie słowa FALSE będzie równoważne z 0, a każde wystąpienie słowa TRUE będzie równoważne z 1. Podmiana następuje przed kompilacją pliku źródłowego.
const typ zmienna = wartość;
W ten sposób tworzymy zmienną, której wartości nie można w programie zmieniać. Zaletą w stosunku do poprzedniego sposobu jest to, iż stałą tworzy kompilator, zatem może wyłapać on różne błędy.
Sens takiej konstrukcji dla prostej zmiennej może nie być zbyt duży. Lecz wyobraź sobie, że chcemy w programie zainicjować np. tablicę struktur (czasem się z takiego tworu korzysta):
/*
Stałe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 30.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
struct triplex
{
float a,b,c;
};
int main()
{
const struct triplex cs = {3.1415,2.7728,1.4312};
struct triplex t[10];
int i;
setlocale(LC_ALL,"");
for(i = 0; i < 10; i++) t[i] = cs;
t[5].a = 6.2833;
t[5].b = 3.0;
t[5].c = 0.22;
for(i = 0; i < 10; i++)
printf("Struktura nr %d\n"
"a = %6.4f b = %6.4f c = %6.4f\n\n",
i, t[i].a, t[i].b, t[i].c);
return 0;
}
|
enum {nazwa, nazwa,...};
enum {nazwa=wartość, nazwa=wartość, ...};
|
Typ wyliczeniowy tworzy automatycznie zestaw stałych typu całkowitego. Stałym można przypisywać kolejne wartości lub wartości wybrane. Utworzone w ten sposób stałe można wykorzystać do dowolnego celu:
/*
Stałe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 30.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
int main()
{
enum {up,down,left,right};
int d;
setlocale(LC_ALL,"");
d = up+down+left+right;
printf("%d\n\n",d);
return 0;
}
|
Literał jest zapisem liczby. Np. 15 jest literałem liczby dziesiętnej. Ponieważ w języku C mogą występować różne typy danych, istnieją dla nich odpowiednie literały:
/*
Stałe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 30.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
int main( )
{
unsigned long long a;
setlocale(LC_ALL,"");
a = 30000000000ul;
printf("%llu\n\n", a);
return 0;
}
|
Wyrażenie jest stałe (ang. constant expression), jeśli jego wartość da się policzyć w czasie kompilacji. Tego typu wyrażenia są przez kompilator obliczane i zamieniane na stałą o wartości wyrażenia. Dzięki temu mikroprocesor nie musi obliczać wartości takiego wyrażenia podczas działania programu. Przyspiesza to wykonywanie programu oraz skraca jego kod.
W świecie mikrokontrolerów wyrażenia stałe często używane są do ustawiania bitów w rejestrach.
Na przykład instrukcja:
r = (1 << 5) | (1 << 3) | (1 << 2); |
jest równoważna instrukcji:
r = 0b101100; |
Pierwszy sposób stosuje się dla poprawienia czytelności operacji.
Najpierw wyjaśnijmy sobie pojęcie liczby losowej (ang. random number) i jej zastosowania.
Liczba losowa to taka, której zakres znamy (np. od 1 do 6 dla oczek wyrzucanych na kostce do gry), lecz nie potrafimy przewidzieć konkretnej wartości, ponieważ jest losowa (jak wynik rzutu kostką – taką niezafałszowaną). Mówimy, że dana wartość pojawia się z pewnym prawdopodobieństwem. Prawdopodobieństwo (a właściwie rachunek prawdopodobieństwa) jest terminem matematycznym pochodzących z teorii gier, która, co ciekawe, wcale obecnie nie służy tylko i wyłącznie do grania w gry, a nawet jest jedną z najważniejszych teorii matematycznych. Rachunek prawdopodobieństwa jest używany przez poważne teorie fizyczne (np. teorię kwantową), ekonomiczne i socjologiczne.
Nie wnikając zbyt głęboko w aspekty matematyczne, określmy prawdopodobieństwo jako liczbę P z zakresu od 0 do 1. Prawdopodobieństwa wiążą się z konkretnymi zdarzeniami:
Prawdopodobieństwo P równe 0 oznacza zdarzenie niemożliwe, czyli takie, które w żaden sposób nie może zajść.
Prawdopodobieństwo P równe 1 oznacza zdarzenie pewne, czyli takie, które wystąpi na pewno.
Jeśli weźmiemy zbiór n zdarzeń (np. 6 zdarzeń wyrzucenia kostką od jednego do 6 oczek) i stanowią one zbiór kompletny (tzn. w danym doświadczeniu nie może wystąpić żadne inne zdarzenie spoza tego zbioru), to suma ich prawdopodobieństw jest równa 1, czyli pewnikiem jest, że zawsze któreś z tych zdarzeń na pewno wystąpi.
Na przykład, przy rzucie kostką mamy 6 możliwych zdarzeń (pomijamy zdarzenia typu: kostkę złapał kot i uciekł z nią):
Każde z tych zdarzeń jest jednakowo prawdopodobne i zakładamy, że zawsze któreś z nich wystąpi. Mamy zatem:

Wynika z tego, że każde ze zdarzeń wypadnięcia określonej
liczby oczek posiada prawdopodobieństwo
Jeśli każde z możliwych zdarzeń ma takie samo prawdopodobieństwo wystąpienia, to mówimy, że rozkład tych prawdopodobieństw jest równomierny.
Jak zauważyłeś, liczby losowe stosowane mogą być do symulacji wyników losowych. Jednak jest z nimi jeden kłopot: nie da się ich tworzyć programowo. Powodem jest to, iż liczba losowa nie jest wynikiem żadnego wyrażenia, w którym nie występują inne liczby losowe. I dostajemy błędne koło.
Jednym ze sposobów tworzenia liczb losowych w mikrokontrolerach jest wykorzystanie tzw. liczników/timerów (są to układy wewnątrz mikrokontrolera, które zliczają impulsy pochodzące z różnych źródeł) oraz współpracy z użytkownikiem. Człowiek dla mikrokontrolera jest bardzo powolną istotą i, co ważniejsze, nieprzewidywalną. Jeśli licznik zlicza impulsy o bardzo dużej częstotliwości, to jego stan zmienia się miliony razy na sekundę. Gdy użytkownik naciśnie np. jakiś przycisk, to mikrokontroler może w tym momencie odczytać stan licznika i otrzymać w ten sposób dobrą liczbę losową (nie wiadomo, kiedy użytkownikowi przyjdzie ochota nacisnąć przycisk, dlatego stan licznika jest nieprzewidywalny, a zatem losowy).
Jednak co zrobić, jeśli nie mamy wolnego licznika, a musimy generować liczby pseudolosowe? Z pomocą przychodzi tzw. generator liczb pseudolosowych.
Generator liczb pseudolosowych (ang. pseudorandom number generator) jest funkcją, która zwraca wartości wyglądające jak liczby losowe (to jak to właśnie łaciński przedrostek pseudo = jakby), lecz nimi nie będące. Jak to rozumieć? Otóż chodzi o to, że taka funkcja tworzy ciąg liczb, które można uznać za losowe, ponieważ na pierwszy rzut oka nie widać pomiędzy nimi jakiś zależności. W rzeczywistości taka zależność jest i jeśli znamy jedną liczbę pseudolosową, to możemy sobie wygenerować wszystkie następne. Co więcej, liczby pseudolosowe tworzą ciąg wartości, który powtarza się po pewnym okresie (np. po wygenerowaniu 4 miliardów liczb, otrzymujemy od nowa ten sam ciąg).
Jeśli cię zainteresował ten temat, to odsyłam do obszernego artykułu o generatorach liczb pseudolosowych, który znajduje się w naszym serwisie.
Wróćmy na ziemię. W języku C masz dostęp do generatora liczb pseudolosowych za pomocą dwóch funkcji:
srand(ziarno); |
| ziarno | – | określa tzw. ziarno liczb
pseudolosowych, czyli wartość na podstawie której zostanie wygenerowany ciąg pseudolosowy. Jest to wartość typu int. |
Funkcja
Jeśli mikrokontroler nie posiada takiego zegara, to można
wykorzystać pośrednio pamięć EPROM, która jest dostępna
praktycznie w każdym mikrokontrolerze. Pamięć EPROM pamięta dane
nawet po wyłączeniu zasilania. W pamięci tej tworzymy prosty
licznik, którego zawartość zwiększamy o 1 (lub
o inną wartość) przy każdym uruchomieniu programu.
Następnie stan tego licznika wykorzystujemy jako ziarno dla
funkcji
rand(); |
Funkcja jest bezargumentowa. Każde wywołanie daje liczbę pseudolosową z zakresu od 0 do 32767, czyli 15 najstarszych bitów wygenerowanej liczby pseudolosowej. Dlaczego tak dziwnie? Chodzi o rozkład. Stwierdzono, że te 15 bitów daje lepszą równomierność generowanych liczb niż inne bity liczby pseudolosowej tworzonej w generatorze.
Obie funkcje są zdefiniowane w pliku nagłówkowym
Uruchom program:
/*
Liczby pseudolosowe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 31.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <locale.h>
#define MAXC 78
int main()
{
int i;
setlocale(LC_ALL,"");
srand((int)time(NULL)); // inicjujemy generator LCG
for(i = 0; i < 40; i++) printf("%d\n",rand());
return 0;
}
|
W programie użyto nowej funkcji
Funkcja
a + rand( ) % (b - a + 1)
|
Na przykład, chcesz generować wyniki rzutów kostką. Wyrażenie
pseudolosowe o wartościach od 1 do 6 jest następujące
| 1 + rand( ) % 6 |
Uruchom program:
/*
Liczby pseudolosowe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 31.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <locale.h>
#define MAXC 78
int main()
{
int i;
setlocale(LC_ALL,"");
srand((int)time(NULL)); // inicjujemy generator LCG
for(i = 1; i <= 40; i++)
printf("Rzut nr %2d, liczba oczek %d\n", i, 1+rand()%6);
return 0;
}
|
Kolejny program "rzuca" kostką 6 milionów razy, i sprawdza, czy wypadło około miliona każdej z możliwych liczby oczek.
/*
Liczby pseudolosowe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 31.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <locale.h>
#define MAXC 78
int main()
{
int t[] = {0,0,0,0,0,0},i;
setlocale(LC_ALL,"");
srand((int)time(NULL)); // inicjujemy generator LCG
for(i = 0; i < 6000000; i++) t[rand()%6]++;
for(i = 0; i < 6; i++)
printf("%d: %7d\n", i + 1, t[i]);
return 0;
}
|
Liczby pseudolosowe często stosujemy do mieszania zawartości tablic. Zadanie to rozwiązujemy następująco:
Uruchom program:
/*
Liczby pseudolosowe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 31.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <locale.h>
int main()
{
int t[] = {0,1,2,3,4,5,6,7,8,9},i,i1,i2,x;
setlocale(LC_ALL,"");
srand((int)time(NULL)); // inicjujemy generator LCG
printf("Przed: ");
for(i = 0; i < 10; i++) printf("%2d",t[i]);
// mieszamy
for(i = 0; i < 30; i++)
{
i1 = rand() % 10; // losujemy indeksy
i2 = rand() % 10;
x = t[i1]; t[i1] = t[i2]; t[i2] = x;
}
printf("\n\nPo : ");
for(i = 0; i < 10; i++) printf("%2d",t[i]);
printf("\n\n");
return 0;
}
|
Za pomocą mieszania możesz napisać prosty program losujący liczby Multilotka (20 z 80).
/*
Liczby pseudolosowe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 31.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <locale.h>
int main()
{
char m[80],r[10],x;
int i,j,k,i1,i2;
setlocale(LC_ALL,"");
// inicjujemy generator LCG
srand((int)time(NULL));
// Wypełniamy tablicę bil m[]
for(i = 0; i < 80; i++) m[i] = i + 1;
for(k = 1; k <= 10; k++)
{
printf("Lowanie Multilotka nr %d\n",k);
// mieszamy bile
for(i = 0; i < 240; i++)
{
// losujemy indeksy
i1 = rand() % 80;
i2 = rand() % 80;
// zamieniamy bile
x = m[i1]; m[i1] = m[i2]; m[i2] = x;
}
// losujemy 20 pierwszych bil
memcpy(r, m, 10);
// sortujemy wylosowane bile rosnąco
for(i = 8; i >= 0; i--)
{
x = r[i];
for(j = i + 1; j < 10; j++)
if(x <= r[j]) break;
else r[j - 1] = r[j];
r[j - 1] = x;
}
printf("Wyniki:");
for(i = 0; i < 10; i++) printf(" %02d", r[i]);
printf("\n\n");
}
return 0;
}
|
Specyfikacja
Inne ciekawe zastosowanie liczb pseudolosowych wiąże się z szyfrowaniem tekstu. W czasie II Wojny Światowej Kwatera Główna Führera stosowała specjalną maszynę szyfrującą o nazwie Ultra.
![]() |
Zasada działania tej maszyny polegała na tym, iż kody znaków były mieszane z ciągiem liczb pseudolosowych, które generowała maszyna na podstawie wprowadzonego wcześniej klucza oraz innych ustawień. My możemy postąpić podobnie: liczby pseudolosowe będzie generował generator pseudolosowy języka C. Kluczem natomiast będzie ziarno dla tego generatora. Jak napisaliśmy wcześniej, jeśli zainicjujemy generator takim samym ziarnem pseudolosowym, to wygeneruje on identyczny ciąg liczb pseudolosowych. Aby się o tym przekonać, uruchom poniższy program:
/*
Liczby pseudolosowe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 31.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <locale.h>
int main()
{
int t = (int)time(NULL); // zapamietujemy czas
int i,j;
setlocale(LC_ALL,"");
for(i = 1; i < 10; i++)
{
srand(t); // ustawiamy generator LCG
for(j = 0; j < 19; j ++) printf(" %03d", rand() % 1000);
printf("\n\n");
}
return 0;
}
|
Program generuje 10 ciągów liczb pseudolosowych. Ponieważ przed generacją każdego ciągu inicjujemy generator pseudolosowy tym samym ziarnem, to generuje on za każdym razem taki sam ciąg liczb.
Na tym fakcie oprzemy algorytm szyfrujący/deszyfrujacy. Kluczem będzie wartość ziarna pseudolosowego jako liczba całkowita. Drugi fakt, to działanie sumy symetrycznej. Jeśli zastosujemy tę operację dwukrotnie z tym samym argumentem, to wynik wróci do postaci wyjściowej:
| 1110001001 | |
| ^ | 0011010111 |
| 1101011110 |
| 1101011110 | |
| ^ | 0011010111 |
| 1110001001 |
Zatem szyfrowanie lub deszyfrowanie polega na operacji różnicy symetrycznej kodu ASCII znaku z liczbą pseudolosową z generatora. Uruchom poniższy program:
/*
Liczby pseudolosowe
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 31.10.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
int main()
{
int key,i;
char t[80];
setlocale(LC_ALL,"");
printf("Wpisz klucz: "); scanf("%d", &key);
printf("Wpisz tekst (do 79 liter):\n\n");
gets(t); // to usuwa znak \n z wejścia
gets(t); // to odczytuje właściwy wiersz
// inicjujemy generator pseudolosowy
srand(key);
// szyfrujemy lub deszyfrujemy
for(i = 0; t[i]; i++) t[i] ^= rand() % 32;
// wyświetlamy wynik:
printf("\n%s\n\n", t);
return 0;
}
|
W programie użyto dwukrotnie funkcji
| Wpisz klucz: 15 Wpisz tekst (do 79 liter): Atak o świcie na pozycje wroga w kwadracie Q17XL. Veqq5e,Ç{~ppl<by.wvdijtp>roodg$~.~}{t~trl` _'!CB0 |
| Wpisz klucz: 15 Wpisz tekst (do 79 liter): Veqq5e,Ç{~ppl<by.wvdijtp>roodg$~.~}{t~trl` _'!CB0 Atak o świcie na pozycje wroga w kwadracie Q17XL. |
Jak widzisz, cała potężna maszyna Ultra sprowadza się we współczesnym świecie do kilku linijek kodu w języku C.
Zwykle zmienna oznacza daną umieszczoną w pamięci komputera. Daną tą inicjuje i zmienia mikroprocesor, gdy życzy sobie tego program. Czasem taką często używaną zmienną mikroprocesor umieszcza w swojej pamięci podręcznej, aby mieć do niej szybki dostęp. Wtedy przy odwołaniu do zmiennej nie sięga on do pamięci, w której zmienna się znajduje, tylko do swojej szybkiej pamięci podręcznej, gdzie ją tymczasowo przechowuje, Zwykle jest to działanie pożądane. Jednak nie zawsze. W mikrokontrolerach w obszarze pamięci znajdują się tzw. rejestry. Zapis lub odczyt danych z tych rejestrów może wywoływać różne działania. Niektóre z tych rejestrów odzwierciedlają poziomy logiczne, które panują na wyprowadzeniach mikrokontrolera lub stan operacji wykonywanych przez urządzenia we/wy, w które są wyposażane mikrokontrolery. Poziomy te mogą z kolei ustawiać urządzenia zewnętrzne, z którymi w danym układzie współpracuje mikrokontroler. Taki rejestr może zmieniać sam swoją zawartość, bez udziału programu. Dodatkowo zmiana ta może się odbywać w dowolnym momencie. Z tego powodu mikrokontroler nie powinien buforować zawartości takiej zmiennej w swojej pamięci podręcznej. Każdy dostęp do zmiennej tego typu musi być wykonany na rzeczywistej komórce pamięci, inaczej mikrokontroler mógłby "przegapić" zmiany wywołane przez urządzenia wewnętrzne lub zewnętrzne.
Na szczęście język C przewiduje takie sytuacje i każdą zmienną możesz opatrzyć słówkiem angielskim słówkiem volatile (ulotny):
volatile typ zmienna; typ volatile zmienna; |
Od tego momentu mikroprocesor będzie zawsze odwoływał się bezpośrednio do zmiennej w pamięci. Nawet, gdy włączysz optymalizacje kodu.
Ze zmiennymi ulotnymi spotkasz się przy programowaniu tzw. przerwań (ang. interrupts).
Język C posiada instrukcję goto, która może wykonać skok w inne miejsce kodu w obrębie funkcji. Najczęściej wykorzystuje się ją przy wychodzeniu z zagnieżdżonych pętli. Najlepszym sposobem używania instrukcji goto jest nieużywanie jej wcale! Jest to opinia doświadczonych programistów. Język C jest językiem strukturalnym, a goto burzy tę cechę. Programy pisane z goto bardzo szybko stają się nieczytelne i co gorsza nieprzewidywalne w działaniu (kod nadużywający goto zamiast rozwiązań strukturalnych nosi nazwę kodu spagetti). Ale skoro taka instrukcja jest, podamy przynajmniej, jak należy ją poprawnie używać.
Składnia jest następująca:
goto etykieta; |
Etykieta jest nazwą, którą umieszczamy w programie w miejscu, do którego należy wykonać skok. Za nazwą etykiety należy wpisać dwukropek. Etykieta musi znajdować się w tej samej funkcji, w której umieszczono rozkaz goto. Nie wolno wykonywać skoków pomiędzy różnymi funkcjami.
/*
Skoki
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 4.11.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
int main()
{
int a;
setlocale(LC_ALL,"");
jeszcze_raz: // tu jest miejsce docelowe skoku
printf("Wpisz liczbe od 1 do 9: ");
scanf("%d", &a);
if(a < 1 || a > 9) goto jeszcze_raz; // skok
printf("Dziekujemy, wpisana liczba to %d\n", a);
return 0;
}
|
To samo da się otrzymać bez używania goto:
/*
Skoki
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 4.11.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
int main()
{
int a;
setlocale(LC_ALL,"");
while(1)
{
printf("Wpisz liczbe od 1 do 9: ");
scanf("%d", &a);
if(a < 1 || a > 9) continue;
printf("Dziekujemy, wpisana liczba to %d\n", a);
break;
}
return 0;
}
|
Wskaźnik jest po prostu adresem obiektu w pamięci, o czym pisaliśmy już wcześniej. Zwykle wskaźnik wskazuje dane, np. liczbę całkowitą, zmiennoprzecinkową, strukturę, tablicę, itp.
Funkcja również jest obiektem w pamięci i posiada swój adres. Pod tym adresem umieszczony jest kod funkcji, czyli ciąg rozkazów dla mikroprocesora, które ten wykonuje, gdy program daną funkcję wywoła. Z tego powodu nic nie stoi na przeszkodzie, aby istniały również wskaźniki do funkcji. Jednak musisz pamiętać, że wskaźnik w języku C to nie tylko sam adres (taki wskaźnik też istnieje: void *). To również typ wskazywanego obiektu. Wskaźnik do danej całkowitej nie jest tym samym co wskaźnik do danej zmiennoprzecinkowej. Chodzi o to, iż kompilator musi wiedzieć, jak ma interpretować daną wskazywaną przez wskaźnik. Również arytmetyka wskaźników odwzorowuję tę cechę: np. dodanie do wskaźnika liczby 1 oznacza przesunięcie jego adresu o tyle, ile wynosi rozmiar wskazywanego obiektu. Jest to bardzo przydatne przy odwoływaniu się do tablic, w których elementy są umieszczone w pamięci jeden obok drugiego.
Aby użyć danej funkcji, kompilator musi wiedzieć o niej kilka rzeczy:
Definicja wskaźnika do funkcji (ang. function pointer) wygląda następująco:
typ_wyniku (* wskaźnik)(typy_argumentów); |
Na przykład, poniższa definicja:
float (*calc)(int *, float); |
Definiuje wskaźnik o nazwie
Wskaźnik
po utworzeniu należy odpowiednio zainicjować. Do tego celu
wykorzystujemy znany nam już operator
wskaźnik = &funkcja; |
Wywołanie funkcji, którą wskazuje wskaźnik wygląda następująco:
wskaźnik(argumenty); |
lub
(* wskaźnik)(argumenty); |
Uruchom poniższy program (przykład nieco naciągany, ale pokazujący ideę stosowania wskaźników do funkcji):
/*
Wskaźniki do funkcji
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 5.11.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
// Zmienna globalna
void (* func)(int);
// Prototypy funkcji
void f1(int);
void f2(int);
// Dwie funkcje, które będzie wskazywał func
void f1(int a)
{
printf("Jestem w funkcji f1( ), a = %d\n", a);
func = &f2;
}
void f2(int a)
{
printf("Jestem w funkcji f2( ), a = %d\n", a);
func = &f1;
}
int main()
{
int i;
setlocale(LC_ALL,"");
func = &f1;
for(i = 1; i < 10; i++) func(i);
return 0;
}
|
Program działa następująco:
W
programie głównym inicjujemy wskaźnik func wpisując do niego
adres pierwszej funkcji
Wskaźniki do funkcji są wykorzystywane przez kilka funkcji bibliotecznych. Jedna z nich realizuje tzw. wyszukiwanie binarne (ang. binary search). Funkcję tę wykorzystuje się na większych mikrokontrolerach, które posiadają dużo pamięci RAM.
Zwykłe wyszukiwanie liniowe (ang. linear search) polega na przeglądaniu kolejnych elementów tablicy i porównywaniu ich z wartością poszukiwaną. Jeśli zostanie znaleziona, to zatrzymujemy dalsze przeszukiwanie tablicy i zwracamy wskaźnik elementu, którego wartość jest równa poszukiwanej. Jeśli natomiast osiągniemy ostatni element element tablicy i poszukiwanej wartości w tablicy nie znajdziemy, to zwracamy wskaźnik pusty NULL.
Dla dużych tablic czas wyszukiwania elementu jest statystycznie liniowo proporcjonalny do ilości elementów n, dlatego algorytm ten nosi nazwę wyszukiwania liniowego. O takim algorytmie mówimy, że ma liniową złożoność obliczeniową. Jeśli liczba elementów tablicy wzrośnie np. 256 razy, to średni czas wyszukiwania elementu również wzrośnie około 256 razy.
Okazuje się, że można znacznie przyspieszyć czas wyszukiwania elementu, jeśli tablica będzie posortowana np. rosnąco. Postępujemy wtedy następująco:
Zakres poszukiwań definiują dwa indeksy: ip (indeks początku) i ik (indeks końca):
| ip | ik |
Komórki tablicy od
Wyznaczamy komórkę leżącą w środku partycji. Jej indeks
oznaczmy jako

| ip | is | ik |
Sprawdzamy, czy komórka
Jeśli nie, to wykorzystujemy fakt uporządkowania tablicy.
Skoro elementy są uporządkowane rosnąco, to na prawo od komórki
| elementy ≤ t[is] | t[is] | elementy ≥ t[is] | ||||||||||||||
| ip | is | ik | ||||||||||||||
Sprawdzamy teraz jak się ma poszukiwana wartość do
| elementy ≤ t[is] | t[is] | elementy ≥ t[is] | ||||||||||||||
|
|
|
|
|
|||||||||||||
Czyli,
Jeśli poszukiwana wartość jest większa od
| elementy ≤ t[is] | t[is] | elementy ≥ t[is] | ||||||||||||||
|
|
|
|
|
|||||||||||||
Zwróć uwagę, że dwa porównania powodują ograniczenie do połowy ilość elementów do przeszukania. Po wyznaczeniu nowej partycji proces poszukiwania powtarzamy aż do znalezienia elementu lub do osiągnięcia partycji pustej.
Okazuje się, że tak skonstruowany algorytm wyszukuje daną w czasie proporcjonalnym do
W bibliotece standardowej (plik nagłówkowy
stdlib.h) jest funkcja
bsearch(poszukiwane, tablica, liczba_elementów, rozmiar, funkcja_porównawcza); |
| poszukiwane | – | wskaźnik elementu, który jest
poszukiwany. Jest to wskaźnik typu const void *, który określa jedynie adres, a nie typ elementu |
|
| tablica | – | wskaźnik pierwszego elementu tablicy. Wskaźnik typu const void *. | |
| liczba_elementów | – | określa liczbę elementów do przeszukania | |
| rozmiar | – | określa rozmiar elementu w bajtach | |
| funkcja_porównawcza | – | wskaźnik do funkcji porównującej
elementy tablicy z wartością poszukiwaną. Ponieważ funkcja bsearch( ) jest uniwersalna, nie określa się typu porównywanych elementów. Wskaźnik wskazuje zatem funkcję o następującej składni:
Funkcja ta jako argumenty przyjmuje dwa wskaźniki
typu void *,
Zobacz na przykładowy program, jak należy tę funkcję |
Funkcja
Uruchom program:
/*
Wskaźniki do funkcji
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 8.11.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <locale.h>
#define MAX 160
// Funkcja porównująca
//--------------------
int comp(const void * a, const void * b)
{
return *(int *) a - * (int *) b;
}
int main()
{
int t[MAX],i,s,*p;
setlocale(LC_ALL,"");
// inicjujemy generator pseudolosowy
srand((int)time(NULL));
// w t umieszczamy ciąg rosnący:
t[0] = rand() % 10; // pierwsza liczba 0...9
for(i = 1; i < MAX; i++)
t[i] = t[i - 1] + (rand() % 4);
// losujemy element
s = t[0] + rand() % (t[MAX - 1] - t[0] + 1);
// szukamy
p = bsearch(&s,t,MAX,sizeof(t[0]),comp);
// wypisujemy wyniki:
for(i = 0; i < MAX; i++)
if(p == &t[i]) printf("[%3d]",t[i]);
else printf(" %3d ",t[i]);
printf("\n\n");
if(p) printf("%d jest w t[%d]\n\n", s , p-t);
else printf("%d nie ma w t[]\n\n",s);
return 0;
}
|
Sortowanie bąbelkowe oraz sortowanie przez wstawianie sortują
tablicę w czasie proporcjonalnym do kwadratu liczby elementów
n2.
Oznacza to, że jeśli zwiększysz liczbę elementów w tablicy 16 razy, to czas sortowania wzrośnie
Algorytm sortowania szybkiego opiera się na zasadzie dziel i zwyciężaj (ang. divide and conquer). W informatyce wiele problemów da się szybciej rozwiązać, jeśli przyjmiemy jakąś mądrą strategię postępowania. Z mojego doświadczenia wynika, że często algorytm prosty działa długo na danym zestawie danych, a algorytm bardziej zaawansowany działa dużo szybciej. Zasada dziel i zwyciężaj składa się z trzech etapów:
Jak to się ma do sortowania? Załóżmy, że mamy partycję startową (patrz poprzedni rozdział) zdefiniowaną przez indeksy ip (początek) i ik (koniec):
| ip | ik |
Partycja ta obejmuje wszystkie elementy tablicy od t[ip] do t[ik]. Jeśli partycja jest pusta, ip > ik, to kończymy. Inaczej w partycji wybieramy dowolny element, który nazwiemy sobie piwotem v (elementem zwrotnym):
| ip | iv | ik |
Następnie elementy partycji przenosimy, tak aby przed piwotem znalazły się elementy mniejsze lub mu równe, a za piwotem elementy większe lub mu równe. Oczywiście końcowa pozycja piwota może ulec zmianie w stosunku do jego pozycji pierwotnej:
|
Partycja lewa elementy ≤ t[iv] |
t[iv] |
Partycja prawa elementy ≥ t[ip] |
||||||||||||||
| ip | iv-1 | iv | iv+1 | ik | ||||||||||||
Zwróć uwagę, że element będący piwotem znajduje się już na swoim właściwym miejscu, tzn. nie musimy go już nigdzie
przenosić w tablicy. Reszta elementów jest częściowo
uporządkowana (względem piwota, lecz nie
względem siebie). Umówmy się, że powstają w ten sposób
dwie nowe partycje: lewa o indeksach
Każdą z nowych partycji sortujemy w ten sam sposób. Za każdym
razem uporządkowanie zbioru rośnie
(rozwiązanie małych problemów, czyli sortowanie partycji,
przybliża nas do rozwiązania problemu głównego, czyli
posortowania całego zbioru). Gdy partycje staną się puste
lub jednoelementowe, sortowanie można przerwać, a zbiór będzie
posortowany. Okazuje się, że tak przeprowadzone sortowanie
wykonuje się w czasie proporcjonalnym
Jeśli cię ten temat zainteresował, to przeczytaj nasz artykuł o sortowaniu szybkim.
W bibliotece standardowej (plik nagłówkowy
qsort(tablica, liczba_elementów, rozmiar, funkcja_porównawcza); |
| tablica | – | wskaźnik pierwszego elementu
tablicy do posortowania. Wskaźnik typu void *. |
| liczba_elementów | – | określa liczbę elementów w sortowanej tablicy |
| rozmiar | – | określa rozmiar elementu w bajtach |
| funkcja_porównawcza | – | wskaźnik do funkcji porównującej
elementy tablicy z wartością poszukiwaną. Ponieważ funkcja qsort( ) jest uniwersalna, nie określa się typu porównywanych elementów. Wskaźnik wskazuje zatem funkcję o następującej składni: int funkcja(const void * a, const void * b); Funkcja ta jako argumenty przyjmuje dwa wskaźniki
typu void *,
|
Funkcja qsort( ) nie zwraca żadnej wartości.
Uruchom program:
/*
Wskaźniki do funkcji
(C)2016 mgr Jerzy Wałaszek
Data utworzenia: 8.11.2016
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <locale.h>
#define MAX 200
// Funkcja porównująca
//--------------------
int comp(const void * a, const void * b)
{
return *(int *) a - * (int *) b;
}
int main()
{
int t[MAX],i;
setlocale(LC_ALL,"");
// inicjujemy generator pseudolosowy
srand((int)time(NULL));
// w t umieszczamy ciąg liczb pseudolosowych 100...999:
for(i = 0; i < MAX; i++) t[i] = 100 + (rand() % 900);
printf("PRZED SORTOWANIEM SZYBKIM:\n\n");
for(i = 0; i < MAX; i++) printf("%4d",t[i]);
printf("\n\n");
// sortujemy szybko
qsort(t,MAX,sizeof(t[0]),comp);
printf("PO SORTOWANIU SZYBKIM:\n\n");
for(i = 0; i < MAX; i++) printf("%4d",t[i]);
printf("\n\n");
return 0;
}
|
To już wszystko w tym kursie. Więcej na temat języka C możesz poznać z licznych kursów w Internecie.
![]() |
Zespół Przedmiotowy Chemii-Fizyki-Informatyki w I Liceum Ogólnokształcącym im. Kazimierza Brodzińskiego w Tarnowie ul. Piłsudskiego 4 ©2026 mgr Jerzy Wałaszek |
Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone pod warunkiem podania źródła oraz niepobierania za to pieniędzy.
Pytania proszę przesyłać na adres email:
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.