Języki programowania
Mikroprocesor wykonując program interpretuje rozkazy zapisane w tzw. kodzie maszynowym, będącym ciągiem bitów zero-jedynkowych. Ze względu na małą czytelność tak zapisanego programu stworzono szereg języków i systemów programowania.
Język definiuje reguły (zasady) zgodnie z którymi powinien być zapisany program, aby możliwe było jego wykonanie, system programowania natomiast służy do tłumaczenia programu na kod maszynowy.
Języki programowania ze względu na sposób złożoności dzieli się na:
niskiego (asembler),
wysokiego (Turbo Pascal, C/C++, Java, Fortran, Visual Basic, Algol)
poziomu. W języku niskiego poziomu większość instrukcji odpowiada pojedynczym rozkazom procesora, dlatego też zapis programu w tym języku jest procesem bardzo pracochłonnym. Języki wysokiego poziomu używają sformułowań stosowanych w życiu codziennym, zaś ich instrukcje zastępują kilka czy nawet kilkanaście instrukcji języka niskiego poziomu.
Generacja kody wynikowego
System programowania do tłumaczenia programu na kod maszynowy używa osobnego programu zwanego translatorem. Ze względu na sposób działania translatory dzieli się na dwie grupy:
interpretery,
kompilatory.
Interpreter po przetłumaczeniu jednej instrukcji programu przekazuje ją do wykonania mikroprocesorowi, po czym tłumaczy kolejną instrukcję programu. Interpreter nie tworzy pliku wynikowego zawierającego kod maszynowy programu (zbiory "exe" lub "com"). Oznacza to, że wykonanie programu jest możliwe tylko w środowisku programowania przy pomocy interpretera.
Kompilator natomiast nie wykonuje programu lecz tłumaczy go na kod maszynowy w całości, w wyniku czego powstaje zbiór wykonywalny przez procesor.
Zmienne w językach programowania
W języku programowania pod pojęciem danych występują takie wartości jak liczby naturalne, liczby całkowite, liczby rzeczywiste, znaki, ciągi znaków, czyli napisy (łańcuchy) posiadające sens lub nie, itp. Z każdym algorytmem (programem) związany jest pewien zbiór danych, który musi zostać odpowiednio przetworzony w wyniku czego wykonanie algorytmu prowadzi do otrzymania wyników. Podczas przetwarzania danych wykonuje się na nich różnego typu operacje jak np. modyfikacja danych czy obliczanie wyników pomocniczych.
Do przechowywania wartości danych używa się w języku programowania zmiennych, które są odpowiednikami występujących w matematyce niewiadomych. Każda niewiadoma ma ściśle określony zbiór z którego może przyjmować wartości, zwany zakresem zmienności zmiennej. Na podstawie tego właśnie zbioru mówimy o zmiennej naturalnej, całkowitej, wymiernej czy rzeczywistej. Takie zbiory występują również w językach programowania i nazywane są typami. W odróżnieniu od matematyki nie mówimy że zmienna X należy do typu A, tylko fakt ten wypowiadamy krócej: X jest typu A.
Typy całkowite języka C/C++
Do przechowywania liczb całkowitych służą w języku C/C++ typy całkowite. W matematyce ze względu na tradycję zbiory danych oznacza sie dużymi literami alfabetu, w informatyce nie ma takich ograniczeń i typy jako zbiory posiadają swoje słowne nazwy.
Język C/C++ posługuje się trzema podstawowymi typami całkowitymi:
Nazwa typu |
Opis typu |
char |
typ reprezentujący wszystkie możliwe do uzyskania znaki, zbiór 256 znaków |
int |
typ służący do reprezentacji liczb całkowitych |
long |
typ reprezentujący duże liczby całkowite |
Każdą z wyżej wynienionych nazw typów można poprzedzić słowem kluczowym signed lub unsigned, co będzie oznaczało odpowiednio zmienną całkowitą ze znakiem lub bez znaku. Otrzymamy wówczas sześć różnych typów całkowitych o zakresach:
Nazwa typu |
Zakres |
Ilość bajtów |
signed char |
-128..127 |
1 |
unsigned char |
0..255 |
1 |
signed int |
-32768..32767 (-216..216-1) |
2 |
unsigned char |
0..65535 (0..216-1) |
2 |
signed long |
-2.147.483.648..2.147.483.647 (-231..231-1) |
4 |
unsigned long |
0..4.294.967.295 (0..232-1) |
4 |
Liczby całkowite bez znaku (dodatnie) są zapisywane w tzw. naturalnym kodzie dwójkowym, w którym wartość liczby obliczana jest jako suma kolejnych potęg liczby 2:
110101 = 1*25+1*24+0*23+1*22+0*21+1*20 = 32+16+4+1 = 53 .
W przypadku liczb ze znakiem najstarszy bit (pierwszy z zapisie) ma wagę ujemną, pozostałe natomiast reprezentują liczbę zapisaną w naturalnym kodzie binarnym. Wartość powyższej liczby liczy się w następujący sposób:
110101 = -1*25+1*24+0*23+1*22+0*21+1*20 = -32+16+4+1 = -11 .
Liczba ośmiobitowa 10000000 ze znakiem ma wartość -128, liczba 11111111 wartość -1. Taki sposób reprezentacji liczb ujemnych nazywamy kodem uzupełnień U2.
Aby znaleźć zapis dwójkowy liczby ujemnej ze znakiem x na n bitach należy wyznaczyć zapis liczby 2n-1-1+x na n-1 bitach, a następnie dopisać bit 1 na pierwszym miejscu jej rozwinięcia dwójkowego.
Przykład
Znaleźć zapis liczby -99 na ośmiu bitach.
Obliczamy najpierw 28-1-1-99=127-99=28. Wyznaczamy zapis liczby 28 na siedniu bitach: 0011100(2)=28 i dopisujemy bit 1: 10011100(2)=-99.
Typy rzeczywiste języka C/C++
W języku C/C++ istnieją trzy podstawowe typy rzeczywiste:
float
double
long double
Zakresy poszczególny typów i ilość pamięci zajmowanej przez zmienne poszczególnych typów są następujące:
Nazwa typu |
Zakres |
Ilość bajtów |
float |
3.4*10-38..3.4*1038 |
4 |
double |
1.7*10-308..1.7*10308 |
8 |
long double |
3.4*10-4932..1.1*104932 |
10 |
Deklaracje zmiennych
Wszystkie używane w programie do przechowywania wartości zmienne muszą być zadeklarowane. Deklaracja zmiennej jest informacją dla kompilatora, aby przydzielił (zarezerwował) odpowiednią ilość pamięci na zmienne, które będą w trakcie wykonywania programu używane.
Język C/C++ jest bardziej elastyczny w tym względzie niż Pascal: zmienne programu można deklarować niemal wszędzie.
Deklaracja zmiennych może mieć postać:
int x, y=1;
float pole, obwod, pi=3.14;
char znak='&', znak1=48, znak2='\60', znak3='\0x32';
Deklarując zmienne podajemy najpierw typ zmiennej, a następnie pooddzielane znakiem przecinka nazwy zmiennych. Podczas deklaracji zmiennej można przypisać dowolną wartość początkowa - deklarację taką nazywamy deklaracją z inicjacją zmiennej.
Stałe liczbowe rzeczywiste podaje się z kropką dziesiętną, natomiast stałe znakowe ujmuje się w pojedyncze apostrofy.
Zmiennej typu char można przypisać wartości całkowitą. W takiej sytuacji kompilator przypisze zmiennej znakowej znak o podanym w postaci liczby kodzie - czynność tą nazywa się automatyczną konwersją (zamianą z jednego typu na inny).
Wyrażenie '\60' oznacza znak o kodzie ósemkowym 60 (6*8+0), zaś wyrażenie '\0x32' znak o kodzie szesnastkowym 30 (3*16+2).
Funkcje w językach programowania
W języku programowania często mamy do czynienia z funkcjami wieloargumentowymi. Przykładem takiej funkcji może być funkcja zwracająca iloczyn liczb i zdefiniowana następująco:
f(x, y) = x * y, dla dowolnych rzeczywistych wartości x i y.
Jest to przykład funkcji dwuargumentowej (dwa argumenty) o argumentach rzeczywistych.
W programach jednak bardzo często używa się funkcji o argumetach nierzeczywistych, czyli nie będących liczbami, np.:
SumaZnakow(znak1, znak2) = znak1znak2
Jest to funkcja posiadająca dwa argumenty typu znakowego i o wartościach będących dwuliterowymi napisami utworzonymi z podanych znaków. I tak np.: SumaZnakow('3', 'A')="3A" (napis po prawej stronie podano w cudzysłowiu, zgodnie z zasadą obowiązującą w języku C/C++).
Funkcja może również posiadać różne typy argumentów - można sobie wyorazić funkcję F, której pierwszym argumentem jest liczba całkowita n, drugim natomiast znak c, zaś wartością funkcji napis składający się z tylu znaków c ile wynosi n. Mielibyśmy wówczas:
F(2, 'A')="AA"
F(5, '8')="88888"
F(0, '?')="" (napis pusty)
Ogólnie: F(n, c)="ccc...c" (n razy)
Definicja matematyczna funkcji zawsze wskazuje dwa zbiory: dziedzinę, czyli zbiór argumentów funkcji oraz przeciwdziedzinę, zwaną też zbiorem wartości.
O dziedzinie mówimy nawet wówczas, gdy tak naprawdę nie jest ona potrzebna: funkcja stała f(x)=2 dla każdego argumentu zawsze posiada wartość 2, tyle tylko, że w matematyce zawsze organiczamy zbiór na którym można obliczyć tą wartość, czyli wskazujemy dziedzinę.
Języki programowania posuwają się nieco dalej i dopuszczają istnienie funkcji o niegraniczonej (nieokreślonej) dziedzinie. O ile w matematyce o funkcji f(x)=2 wypada powiedzieć, że jej wartością jest liczba 2 dla każdego argumentu będącego liczbą rzeczywistą, o tyle w języku C/C++ można powiedzieć, że wartością tej funkcji jest liczba 2 dla każdego możliwego argumentu, czyli...
f(123)=2
f('K')=2
f("Borland C/C++")=2
f(monitor mojego komputera)=2
Funkcję taką (o nieokreślonej dziedzinie) nazywamy bezargumentową lub bezparametrową.
W matematyce każda funkcja f ma jednoznacznie określony zbiór wartości (przeciwdziedzinę) - wyrażenie f(x), gdzie x jest elementem dziedziny jest wartością funkcji dla argumentu x, a tym samy elementem zbioru będącego przeciwdziedziną.
Z taką samą sytuacją mamy doczynienia w języku C/C++. Wszystkie funkcje języka mają jednoznacznie określony zbiór wartości (przeciwdziedzinę). O funkcji f, której zbiorem wartości jest zbiór Y mówimy, że f jest typu Y, lub też, że f zwraca wartość typu Y.
Jezyk C/C++ w przypadku przeciwdziedziny posuwa się nieco dalej niż niż definicja matematyczna, a mianowicie przeciwdziedzina może być zbiorem nieokreślonym, tak samo jak miało to miejsce w przypadku dziedziny. Dla funkcji f o wartościach typu int wyrażenie f(x) jest wartością typu int, dla funkcji o wartościach nieokreślonych, wyrażenie f(x) jest nieokreślone.
Funkcję taką nazywamy funkcją nieokreśloną (o nieokreślonym typie wyniku).
Podsumowując:
każda funkcja języka C/C++ określona jest podobnie jak w matematyce:
f : X --> Y
gdzie X i Y są dowolnymi typami danych, przy czym oba zbiory (typy) X i Y mogą być nieokreślone. Typ nieokreślony języka C/C++ nosi nazwę void.
Definiując funkcję należy wyraźnie wskazać (określić) zbiór dozwolonych argumentów (dziedzinę) oraz typ wyniku funkcji - jeżeli natomiast funkcja nie posiada argumentów lub nie zwraca żadnego wyniku, to każdy z tych zbiorów określamy jako void.
Ogólna postać definicji funkcji jest następująca:
typ_wyniku nazwa_funkcji(lista argumentów)
{
instrukcja1;
instrukcja2;
:
instrunkcjak;
}
Linię pierwszą nazywamy nagłówkiem funkcji, zaś wszystkie pozostałe ciałem funkcji (blokiem). Blok składa się z ciągu instrukcji języka ujętych w nawiasy klamrowe i pooddzielanych od siebie znakiem średnika.
Przykłady definicji funkcji:
long double suma_kwadratow(double a, double b)
{
return a*a+b*b;
}
float stala_pi(void)
{
return 3.14;
}
void czysc(void)
{
clrscr();
}
Pierwsza ze zdefiniowanych funkcji suma_kwadratow zwraca wartość (liczbę) typu long double będącą sumą kwadratów dwóch liczb rzeczywistych typu double.
Funkcja stala_pi nie posiada arguentów (bezparametrowa), zwraca natomiast wartość typu float będącą przybliżeniem liczby pi.
Funkcja czysc jest bezwartościowa (nie posiada wartości) i bezparametrowa (nie posiada argumentów) - jej wykonanie polega na wykonaniu jednej umieszczonej w ciele funkcji o nazwie clrscr.
W pierwszych dwóch funkcjach występuje instrukcja return. Jej wykonanie, powoduje natychmiastowe zakończenie wykonywania funkcji. Jeśli definiowana funkcja posiada określony typ wyniku, to po słowie return należy podać wyrażenie, którego wartość stanie się wartością definiowanej funkcji. W przypadku takiej funkcji konieczne jest wykonanie instrukcji return, w przeciwnym przypadku kompilator nie byłby w stanie określić wartości funkcji, a tym samym przypisać jej wyniku.
Jeśli funkcja nie posiada określonego typu wyniki, to w wywołaniu return nie umieszczamy żadnej wartości - nie można przypisać wartości funkcji, która zadeklarowana została jako nieokreślona. Można również w bloku funkcji nie umieszczać instrukcji return - wówczas wykonywanie funkcji kończy się w momencie wykonania wszystkich instrukcji zawartych w jej ciele.
Budowa programu
Funkcje są chyba najistotniejszym elementem języka C/C++. Każdy napisany w tym języku program składa się z modułów (bibliotek, może być jeden), w których zdefiniowane zostały różne funkcje. Jeden z dołączonych do programu modułów musi zawierać tzw. funkcję główną o nazwie main, od której rozpoczyna się wykonywanie programu.
Najprostszy wykonywalny program języka ma postać:
void main(void)
{
}
i zawiera tylko definicję (pustą) funkcji głównej. Niestyty nic jeszcze nie robi, ponieważ w bloku nie umieszczono żadnych instrukcji przeznaczopnych do wykonania.
Operacje wejścia-wyjścia
Wprowadzanie danych do programu i wyprowadzanie wyników z programu realizuje się w języku C/C++ za pomocą tzw. strumieni. Standardowo zdefiniowano trzy strumienie wejścia-wyjścia:
stdin - standardowy strumień wejściowy
stdout - standardowy strumień wyjściowy
stderr - standardowy strumień błędów
Obsługą tych strumieni (wczytywaniem i wysyłaniem danych) zajmują się funkcje języka zdefiniowane w module (bibliotece) o nazwie stdio.h.
Wyprowadzanie danych na standardowe wyjście programu (stdout, na ogół ekran monitora) wykonuje funkcja o nazwie printf. W najprostszej postaci funkcję tą można wywołać podając jako jedyny argument łańcuch (napis):
#include<stdio.h>
void main(void)
{
printf("To jest mój pierwszy program...\nI nic jeszcze nie robi...");
}
Moduł do programu dołącza się za pomocę dyrektywy (polecenia) kompilatora include - deklarację dołączenia modułu umieszczamy na ogół na początku pliku źródłowego programu.
Funkcja printf może mieć wiele argumentów, które ponadto mogą posiadać różne typy danych. Spośród wszystkich argumentów najważniejszą rolę pełni argument pierwszy, zwany łańcuchem formatującym. Łańcuch oprócz zwykłych wartości znakowych (liter i znaków) może zawierać znaki lub tzw. sekwencje formatujące - w powyższym przykładzie łańcuch formatujący zawiera znak specjalny '\n', będący poleceniem nowego wiersza (nowej linii). Mówiąc dość ogólnie: funkcja printf wypisze na standardowym wyjściu wszystkie znaki łańcucha formatującego, łącznie ze znakiem '\n', którego wypisanie polega na przeniesieniu kursora tekstowego na początek nowego wiersza ekranu.
A oto inny przykład łańcucha formatującego:
printf("Mam na imię %s, mam %d lat.\n", "Tomasz", 18);
Powyższe wywołanie funkcji zawiera aż trzy argumenty: łańcuch formatujący, łańcuch "Tomasz" oraz liczbę całkowitą 18. W łańcuchu formatującym występują dwie sekwencje formatujące: %s oraz %d. Pierwsza z nich jest dla kompilatora informacją, że w to miejsce należy podstawić napis podany jako drugi argument funkcji, druga sekwencja %d informuje kompilator, że w miejsce jej wystąpienia należy wstawić liczbę całkowitą ze znakiem będącą trzecim argumentem funkcji.
W efekcje wykonania powyższych czynności na ekranie monitora zostanie wypisany tekst: Mam na imię Tomasz, mam 18 lat..
UWAGA:
Kolejność wszystkich argumentów funkcji oprócz pierwszego musi być dostosowana do sekwencji formatujących zawartych z łańcuchu formatującym. Przestawienie w powyższym przykładzie imienia "Tomasz" i liczby 18:
printf("Mam na imię %s, mam %d lat.\n", 18, "Tomasz");
spowoduje, że kompilator podejmie próbę odczytania liczby jako napisu i napisu jako liczby, co przyniesie dosyć dziwne skutki i raczej na pewno nie zostanie wykonanie poprawnie (można spróbować).
W łańcuchu formatującym można używać następujących sekwencji formatujących:
%d - liczba całkowita ze znakiem
%i - liczba całkowita ze znakiem
%o - liczba ósemkowa bez znaku
%u - liczba całkowita bez znaku
%x - liczba szesnastkowa bez znaku
%X - liczba szesnastkowa bez znaku
%f - liczba rzeczywista zapisana w postaci [-]ddd.ddd
%e - liczba rzeczywista zapisana w postaci wykładniczej
%c - znak
%s - łańcuch (napis)
Pomiędzy znakiem % a literą wskazującą typ argumentu mogą wystąpić dwa przydatne specyfikatory określające szerokość pola i ilość miejsc po przecinku:
printf("%10.3f", 1.4242135);
Liczba przed kropką wskazuje szerokość pola, czyli ilość komórek na ekranie, które zajmie wypisywana wartość (jeśli jest krótsza na początku wypisane zostaną spacje), liczba po kropce określa dokładność, czyli ilość miejsc po przecinku. W przypadku argumentów w stosunku do których nie można określić dokładności (np. znak, napis, liczba całkowita) nie podaje się znaku kropki i liczby po niej występującej, można określić jedynie szerokość pola.
Przed literą określającą typ argumentu można jeszcze umieścić tzw. modyfikator rozmiaru. W przypadku typów całkowitych umieszczenie litery 'l' (np. %ld, %lu) spowoduje potraktowanie argumentu jako long (czyli odpowiednio long i unsigned long), w przypadku argumentów rzeczywistych (np. %lf) jako double.
Liczbę typu long double należy wypisać używając sekwencji %Lf lub %Le.
Funkcja służąca do wprowadzania danych ze standardowego wejścia stdin nosi nazwę scanf i również zdefiniowana jest w module stdio.h. Ogólna składnia funkcji nie odbiega od składni funkcji printf - pierwszy argument przeznaczony jest na łańcuch formatujący, pozostałe, na wczytane przez funkcję wartości.
W łańcuchu fomatującym można używać następujących sekwencji określających typ wczytywanej wartości:
%d - liczba całkowita (dziesiętna) ze znakiem
%o - liczba ósemkowa bez znaku
%x - liczba szesnastkowa bez znaku
%i - liczba dziesiętna, ósemkowalub szesnastkowa
%f - liczba rzeczywista typu float
%e - liczba rzeczywista typu float
Podobnie jak w przypadku funkcji printf znak oreślający typ można poprzedzić modyfikatorem rozmiaru 'l' lub 'L' (np. %lu, %Le).
W odróżnieniu od funkcji printf funkcja scanf wymaga oprócz łańcucha formatującego argumentów w postaci adresów zmiennych (tzw. wskaźników). Na obecnym etapie będzie dosyć trudne zrozumienie istoty wskaźnika, dlatego też należy przyjąć poniższy sposób podawania argumentów funkcji jako konieczność:
printf("Podaj wartość zmiennej rzeczywistej x=");
//odczytaj wartość typu long double
scanf("%Lf", &x);
printf("Podaj nieujemną wartość całkowitą k=");
//odczytaj wartość typu unsigned int
scanf("%u", &k);
WAŻNE
Obie funkcje wejścia-wyjścia posiadają więcej opcji stwarzających programiście dodatkowe możliwości - początkującym programistom zaleca się jednak wykorzystywanie tylko tych podstawowych, w jak najprostszej możliwej postaci, czyli tak, jak pokazuje to powyższy przykład.
Linie programu poprzedzone parą znaków '/' oznaczają komentarz i podczas wykonywania programy nie są brane pod uwagę. Dłuższe komentarze (kilkuwierszowe) można umieścić w programie ujmując tekst w wyrażenia '/*' oraz '*/':
/*
komentarz
komentarz
komentarz
*/
Pierwszy program w języku C/C++
Może to wydać nawet dziwne, ale znając zaledwie dwie instrukcje języka można napisać niebagatelny program, przynajmniej na tyle, że napisanie tego samego w języku Pascal wymagałoby znajomości wielu instrukcji z pętlami włącznie.
Zadanie
Napisać program, który wczyta nieujemną liczbę całkowitą, wypisze jej wartość ósemkową i szesnastkową, następnie wczyta liczbe szesnastkową i wypisze jej wartość dziesiętną.
#include<conio.h>
void main(void)
{
unsigned long x;
clrscr();
printf("Podaj nieujemną liczbę całkowitą x=");
scanf("%lu", &x);
printf("Dziesiętnie : %lu\n", x);
printf("Ósemkowo : %lo\n", x);
printf("Szesnastkowo : %lx\n", x);
printf("\nPodaj liczbę szesnastkową x=");
scanf("%lx", &x);
printf("Dziesiętnie : %lu", x);
printf("\nNaciśnij jakiś klawisz...");
getch();
}