Wstęp do programowania w języku C/C++
Pojęcie algorytmu
Programowanie komputerów jest podstawową dziedziną informatyki i obejmuje:
• projekt programu - konstrukcja algotytmu,
• zapis programu w dowolnym języku programowania,
• testowanie programu.
Algorytmem nazywamy sposób postępowania (metodę) z danymi, który w skończonej liczbie kroków
(czynności) prowadzi do otrzymania oczekiwanych wyników.
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. Odp: 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();
}
Operatory języka C/C++
Właściwości operatorów
Operatory (symbole działań) języka C/C++ tworzą znaczące dla programisty narzędzie umożliwiające
wykonywania czynności, które w innych językach programowania wymagałyby definiowania funkcji czy
wykonywania tych samych czynności w bardzie pracochłonny sposób.
Cechą charakterystyczną każdego operatora jest jego arność, czyli ilość argumentów. W matematyce
korzystamy na ogół z operatorów dwuargumentowych (binarnych), takich jak +, -, *, /, rzadziej z operatorów
jednoargumentowych (unarnych), którego przykładem może być operator '!' przy pomocy którego zapisujemy
działanie silnia (n!=1*2*...*n).
W języku C/C++ oprócz operatorów unarnych i binarnych występuje również operator trzyargumentowy, zwany
operatorem wyboru.
Każdemu operatorowi można przypisać liczbę naturalną zwaną priorytetem - wartość tej liczby odwzorowuje
matematyczną kolejność wykonywania działań - operatory o priorytecie wyższym oznaczają działania
wykonywane wcześniej. Wyrażenie a-b*c zostanie zrozumiane przez kompilator jako a-(b*c) ponieważ operator
'*' ma wyższy priorytet niż operator '-'.
Wyobraźmy sobie funkcję f zdefiniowaną następująco:
| 0, gdy x<>0,
f(x) = |
| 1, gdy x=0.
Język C/C++ zawiera jednoargumentowy operator negacji '!' będący odpowiednikiem tak zdefiniowanej funkcji.
W odróżnieniu od matematyki zapis działania z tym operatorem zapisujemy w odwrotny sposób, czyli !n, a nie
jak w matematyce n!.
Tą cechę operatora (cechę wskazującą na położenie argumentów) nazywamy wiązaniem. Powiemy, że w
matematyce operator silnia ma wiązanie lewostronne, odnosi się do wartości występującej po jego lewej stronie,
w języku C/C++ operator negacji ma wiązanie prawostronne, związany jest z wartością po stronie prawej.
Uwaga:
W poniższym tekście opisano jedynie wybrane operatory języka, których znajomość jest niezbędna do
zrozumienia dalszej części dokumentu.
Operator przypisania
Jednym z najniższych priorytetów (2) charakteryzuje się operator przypisania '=', służący do nadawania wartości
zmiennym. W języku Pascal czynność tą wykonuje się za pomocą instrukcji zwanej instrukcją przypisania -
język C/C++ wykonuję tą czynność przy pomocy operatora.
Niski priorytet tego operatora gwarantuje m. in. poprawność wykonania instrukcji:
x=y+1
a=b*!c
W obu przykładach najpierw zostaną wykonane działania po prawej stronie operatora, a następnie obliczona
wartość zostanie przypisana podanej po lewej stronie zmiennej - dzieje się tak dlatego, że operatory występujące
po prawej stronie znaku '=' mają wyższy priorytet niż operator '='.
Operatory arytmetyczne
Do podstawowych operatorów arytmetycznych należą operatory addytywne dodawania i odejmowania ('+' i '-') o
priorytecie 12.
Tymi samymi znakami określa się również operatory unarne wskazujące znak zmiennej (+x, -x), w tym jednak
wypadku priorytet obu operatorów jest bardzo wysoki i wynosi 15. W wyrażeniu:
x=+y+-z
pierwszy znak '+' oznacza operator unarny (operator znaku) i ma priorytet 15, drugi znak '+' zostanie
zinterpretowany jako operator binarny oznaczający dodawanie o priorytecie 12, zaś znak '-' jako operator znaku
o priorytecie 15. Ostatecznie całe wyrażenie będzie odczytane jako x=(+y)+(-z).
Drugą grupę operatorów arytmetycznych stanowią operatory mutiplikatywne mnożenia, dzielenia i reszty z
dzielenia ('*', '/' i '%'). Operatory multiplikatywne posiadają priorytet 13, czyli wyższy niż addytywne, niższy
jednak niż operator negacji.
Uwaga
W języku C/C++ nie mam operatora dzielenia całkowitego, ponieważ każde dzielenie, którego argumentami są
wyrażenia całkowite posiada wynik całkowity, czyli wykonywane jest dzielenie z resztą, a nie dzielenie zwykłe.
Na przykład po deklaracjach:
signed int a=12;
float x=a/5;
----------------
signed int a=12;
float x=a/5.0;
zmienna x otrzyma w pierwszym przypadku wartość 2, w przypadku drugim wartość 2.4 (dzielnik zapisany z
kropką traktowany jest jako rzeczywisty).
Trzecia grupa operatorów arytmetycznych to operatory unarne inkrementacji i dekrementacji ('++' i '--') o
priorytecie 15, wykonujące działanie polegające na powiększeniu lub pomniejszeniu wartości zmiennej o 1.
Każdy z tych operatorów może wystąpić w wyrażeniu jako przedrostkowy lub przyrostowy, tzn. (++x, --x, x++,
x--). Operator przedrostkowy posiada wiązanie prawostronne, przyrostkowy lewostronne.
Ale nie jest to jedyna różnica związana w miejscem wystąpienia operatora. Kluczowe znaczenie ma to wówczas,
gdy operatory występują w wyrażeniu, np.
int m=5, n=2;
int x, y;
x=(--m)+(n++);
y=m*n;
Ufff... Operator przedrostkowy wykonywany jest przed obliczeniem wartości wyrażenia - w pierwszej kolejności
zostanie wykonane działanie --m, które spowoduje pomniejszenie wartości zmiennej m o jeden z 5 na 4.
Następnie nastąpi obliczenie wartości (4+2) i przypisanie jej zmiennej x. Po wykonaniu przypisania zostanie
wykonane działanie n++, które powiększy zmienną n o jeden z 2 na 3. W wyniku wykonania kolejnej instrukcji
zmienna y otrzyma wartość 12 (4*3).
Podsumowując: działanie z użyciem operatora przedrostkowego wykonywane jest przed obliczeniem wartości
wyrażenia, działanie z użyciem operatora przyrostkowego po obliczeniu wartości całego wyrażenia.
Początkującym użytkownikom odradza się budowanie skomplikowanych wyrażeń, co może prowadzić do
trudnych do wykrycia błędów.
Operatory bitowe
Operacje bitowe na liczbach całkowitych wykonuje się za pomocą operatorów:
• & - koniunkcja, "i", priorytet 8,
• | - alternatywa, "lub", priorytet 6,
• ^ - różnica symetryczna, "różne", priorytet 7,
• ~ - negacja, "nie", priorytet 15.
Operator ~ wykonuje negację wszystkich bitów, bity o wartości 0 są zamieniane na 1 i odwrotnie, np. ~128 jest
równe 127, bo 128=10000000, zaś 127=01111111.
Operator | ustawia bit wyniku na 1 jeśli przynajmniej jeden z odpowiadających bitów w argumentach ma
wartość 1, np. 13 | 129 jest równe 10001101=141, bo 13=00001101 oraz 129=10000001.
Operator & ustawia bit wyniku na 1 jeśli oba z odpowiadających bitów w argumentach są równe 1, np. 128 &
127 jest równe 0, a 13 & 129 jest równe 00000001=1.
Operator ^ ustawia bit wyniku na 1 jeśli odpowiadające bity argumentów są sobie różne, np. 128 ^ 127 jest
równe 255, 73 ^ 73 jest równe 0, 13 ^ 129 jest równe 10001100=140.
Operatory przesunięcia bitowego
Operacje przesunięć bitowych wykonujemy za pomocą operatorów:
• << - przesunięcie bitowe w lewo, priorytet 11,
• >> - przesunięcie bitowe w prawo, priorytet 11.
Operatory przesunięcia bitowego posiadają dwa argumenty, z których lewy jest wyrażeniem całkowitym, prawy
natomiast określa ilość miejsc, o którą należy wykonać przesunięcie.
W przypadku liczb bez znaku wolne miejsca w obu przypadkach wypełniane są zerami. W przypadku liczb ze
znakiem wolne miejsca wypełniane są zerami przy przesunięciu w lewo. Podczas przesuwania w prawo wolne
miejsca wypełniane będą takim bitem jaki aktualnie występuje w bicie znaku (najstarszy bit). Przykładowo:
39<<2 jest równe 156, bo 39=00100111 a 156=10011100, 128>>6 jest równe 2.
Niech dana będzie zmienna x typu unsigned int o wartości 11447. Zmienna ta zajmuje 2 bajty pamięci, które
nazywamy odpowiednio starszym i młodszym bajtem. Dwójkowy zapis tej zmiennej ma następującą postać
00101100 10110111. Bajt starszy ma wartość 00101100 czyli 32+8+4=44, bajt młodszy 10110111 wartość
128+32+16+4+2+1=183. Nie jest przypadkiem, że wyrażenie 256*44+183 jest równe 11447. Mając daną
zmienną n typu unsigned int wartość jej starszego bajtu można otrzymać jako wynik dzielenia całkowitego x /
256, wartość bajtu młodszego jest natomiast resztą z tego dzielenia, czyli x % 256.
Tą samą regułę można zastosować w drugą stronę a mianowicie, jeżeli zmienna a zawiera wartość starszego
bajtu pewnej liczby x typu unsigned int, zmienna b wartość młodszego bajtu tej liczby, to wyrażenie 256*a+b
jest równe liczbie x.
Często zachodzi potrzeba odwołania się (sprawdzenia lub zmiany wartości) do poszczególnych bitów zmiennej.
Niech dana będzie zmienna x typu unsigned int. Ostatni najmłodszy bit tej zmiennej można otrzymać wykonując
operację bitową z liczbą 1:
x = aaaaaaaa aaaaaaab
1 = 00000000 00000001
---------------------------
x & 1 = 00000000 0000000b
Pierwsze piętnaście bitów wyniku bedzie posiadać wartość 0, ponieważ bity liczby 1 są równe zero. Natomiast
bit ostatni wyniku otrzyma taką wartość, jaką posiada bit ostatni zmiennej x. Łącznie w wyniku otrzymamy
liczbą zero lub jeden, czyli liczbowo będzie to wartość równa ostatniemu bitowi zmiennej x.
Podobnie należy postąpić chcąc otrzymać np. 5 bit (od końca) zmiennej:
x = aaaaaaaa aaabaaaa
1<<4 = 00000000 00010000
------------------------------------
x & (1<<4) = 00000000 000b0000
------------------------------------
(x & (1<<4))>>4 = 00000000 0000000b
Po wykonaniu operacji bitowej & otrzymamy liczbę zawierającą co najwyżej jeden bit o wartości 1: liczbowo
będzie to wartość równa 0 lub 24, w zależności od wartości bitu b. Wykonując następnie przesunięcie bitowe o 4
w prawo otrzymamy podobnie jak poprzednio liczbę zero lub jeden,czyli wartość piątego od końca bitu.
Zauważyć należy, że można zmienić kolejność operacji wykonując najpierw przesunięcie bitowe o 4, a następnie
operację & z liczbą 1.
Ogólnie: n-ty od końca bit zmiennej x można otrzymać za pomocą wyrażenia:
x >> (n-1) & 1
Zadanie
Dana jest zmienna całkowita x typu unsigned int oraz liczba całkowita n z zakresu 1..16 będąca numerem bitu
zmiennej x (od końca).
1. Jakie operację należy wykonać na zmiennej x, aby jej n-ty bit otrzymał wartość 1.
2. Jakie operację należy wykonać na zmiennej x, aby jej n-ty bit otrzymał wartość 0.
Operatory relacyjne (porównań)
Do porównywania wartości liczbowych i znakowych stosuje się operatory:
• < - mniejsze, priorytet 10,
• <= - mniejsze lub równe, priorytet 10,
• > - większe, priorytet 10,
• >= - większe lub równe, priorytet 10,
• == - równe, priorytet 9,
• != - różne, priorytet 9.
Ważne:
Każde z możliwych porównań, np. a<=10 jest wyrażeniem posiadającym wartość - jeżeli porównanie jest
prawdziwe, to wartością wyrażenia jest liczba 1, jeśli fałszywe wyrażenie ma wartość 0.
Operatory logiczne
Język programowania Pascal zawiera typ logiczny Boolean zawierający dwie wartości True i False, na których
można wykonywać operacje logiczne. W języku C/C++ operacje logiczne wykonywane są na wartościach
liczbowych, przy czym każdą wartość różną od zera uważa się za prawdziwą - wartość zero jest odpowiednikiem
fałszu.
Operacje logiczne wykonuje się za pomocą operatorów:
• || - alternatywa,, priorytet 4,
• && - koniunkcja, priorytet 5,
• ! - negacja, priorytet 15.
Operator wyboru (warunkowy)
Operator warunkowy posiada następującą składnię:
wyrażenie ? wartość gdy prawda : wartość gdy fałsz
Pierwszy argument może być dowolnym wyrażeniem numerycznym lub wyrażeniem, które można przekształcić
do postaci numerycznej, np. nierównością lub porównaniem.
Wykonanie działania operatora polega na obliczeniu wartości podanego wyrażenia i porównaniu obliczonej
wartości z liczbą zero:
• jeśli obliczona wartość jest różna od zera, to nastąpi obliczenie wyrażenia wartość gdy prawda i
zwrócenie obliczonej wartości jako wartość całego wyrażenia,
• jeśli obliczona wartość jest równa zero, to nastąpi obliczenie wyrażenia wartość gdy fałsz i zwrócenie
obliczonej wartości jako wartość całego wyrażenia.
Przykłady:
x = y>=0 ? 1 : 0;
a = x<10 ? x+1 : x++;
printf("%s", x==0 ? "Równe zero" : "Różne od zera");
Złożone operatory przypisania
W języku C/C++ podobnie jak w języku Pascal poprawne są instrukcje postaci x:=x+2 (dla przypomnienia:
instrukcja ta powoduje powiększenie aktualnej wartości zmiennej x o dwa). Efekt uzyskujemy korzystając z
operatora +=:
x += 2; //powiększ x o 2
x -= 3; //pomniejsz x o 3
x *= 4; //pomnóż x przez 4
x /= 5; //podziel x przez 5
x %= 6; //przypisz zmiennej x resztę z dzielenia x przez 6
itd.
Dozwolone są również operacje wykonywane przy użyciu operatorów &=, |=, ^=, <<=, >>=.
Wszystkie operatory przypisania mają niski priorytet 2.
Ważne:
W języku C/C++ operator przypisania, tym samym wyrażenie x=... posiada swoja wartość. Wykonanie działania
przypisania polega na obliczeniu wyrażenia po prawej stronie operatora i przypisaniu obliczonej wartości
zmiennej występującej po lewej stronie operatora. Wartością całego wyrażenia jest wartość przypisana zmiennej.
Konsekwencją tej właściwości jest poprawność instrukcji:
x=y=1; //obie zmienne otrzymają wartość 1
a=1+(b=2); //zmienna b otrzyma wartość 2, zmienna a wartość 3
x=y ? x++ : y++; //przypisz zmiennej x wartość zmiennej y, jeśli będzie to
wartość
//różna od zera, to powiększ zmienną x o jeden, jeśli
będzie to
//wartość zero, to powiększ zmienną y
Przykład 1
Napisz program, który wczyta liczbę całkowitą typu unsigned int a następnie wyznaczy dwójkowy zapis tej
liczby.
#include<stdio.h>
void main(void)
{
unsigned int x;
printf("Podaj liczbę całkowitą = ");
scanf("%u", &x);
printf("Dwójkowo = ");
printf("%u", x & (1<<15) ? 1 : 0);
printf("%u", x & (1<<14) ? 1 : 0);
printf("%u", x & (1<<13) ? 1 : 0);
printf("%u", x & (1<<12) ? 1 : 0);
printf("%u", x & (1<<11) ? 1 : 0);
printf("%u", x & (1<<10) ? 1 : 0);
printf("%u", x & (1<<9) ? 1 : 0);
printf("%u", x & (1<<8) ? 1 : 0);
printf("%u", x & (1<<7) ? 1 : 0);
printf("%u", x & (1<<6) ? 1 : 0);
printf("%u", x & (1<<5) ? 1 : 0);
printf("%u", x & (1<<4) ? 1 : 0);
printf("%u", x & (1<<3) ? 1 : 0);
printf("%u", x & (1<<2) ? 1 : 0);
printf("%u", x & (1<<1) ? 1 : 0);
printf("%u", x & (1<<0) ? 1 : 0);
printf("\n");
}
Konwersje typów
Konwersje (czyli zamiany) typu wyrażenia na inny bardz często wykonywane są w sposób niejawny
(automatyczny). Najprostszym przykładem takich konwersji są poniższe deklaracje:
unsigned char znak1 = 64;
unsigned char znak2 = 300;
signed int x = 3.14;
signed int y = znak1+i;
Inicjacja zmiennej znak1 będzie wymagać konwersji z typu int do typu unsigned char - liczba 64 zamieniona
zostanie na znak o kodzie 64.
Inicjacja zmiennej znak2 również wiąże się z wykonaniem tej samej konwersji, wcześniej jednak liczba 300
zostanie zmieniejszona do pewnej wartości z zakresu 0..255 - zmniejszenie to wykonywane jest poprzez
odrzucenie starszego bajtu tej liczby - w wyniku otrzymana zostanie wartość 44, którą to następnie kompilator
zamieni na znak o kodzie 44.
Trzecia deklaracja wymaga wykonania konwersji z typu float do typu signed int - konwersje tego typu, tj. z
typów rzeczywistych do całkowitych wykonuje się poprzez odrzucenie części ułamkowej - zmienna x otrzyma
wartość 3.
Ostatnia deklaracja wymaga zamiany znaku znak1 na wartość liczbową typu unsigned int, zmiany typu zmiennej
x z signed int na unsigned int, wykonania działania w wyniczego czego otrzymana zostanie wartość typu
unsigned int, a następnie konwersji otrzymanego wyniku do typu signed int.
Ostatni przykład pokazuje jak bardzo jest to skomplikowane zagadnienie. Początkujący programista nie musi
znać wszystkich zasad związanych z konwersją danych, aczkolwiek te, które są niezbędne przytaczam poniżej:
• każde wykonywane działanie wykonuje się zawsze na argumentach tego samego typu, przyprowadzane
z związku z tym automatyczne konwersje wykonuje się zawsze do typów większych,
• konwersja z typu rzeczywistego do całkowitego polega na obcięciu części ułamkowej,
• konwersje pomiędzy różnymi typami rzeczywistymi wykonywane są poprzez zaokrąglenie do
najbliższej dozwolonej wartości typu wynikowego (np. z long double do double),
• wartości typów signed char i unsigned char przekształcane są do typów odpowiednio signed int i
unsigned int,
• jeśli wyrażenie zawiera tylko argumenty całkowite, to wszystkie one zostaną przekształcone do typu
argumentu o największym rozmiarze (nie do końca jest to prawdą, ale można tak przyjąć),
• jeśli jednym argumentem wyrażenia jest liczba całkowita bez znaku, zaś drugim liczba całkowita ze
znakiem o takim samym rozmiarze (np. unsigned long i signed long), to agrument drugi zostanie
przekstałcony do typu bez znaku.
Operator konwesji
I pozostał do wyjaśnienia jeszcze jeden problem, a mianowicie dzielenie liczb całkowitych o wyniku będącym
liczbą rzeczywistą.
unsigned long a, b;
doule x;
scanf("%lu", &a);
scanf("%lu", &b);
x = a / b;
Abu zmienna x otrzymała rzeczywisty wynik dzielenia należy wykonać jawna konwersję przynajmniej jednego z
argumentów a i b do typu rzeczywistego double. Czynność tą wykonuje się za pomocą operatora konwersji (typ
wynikowy)wyrażenie o bardzo wysokim priorytecie 15.
Instrukcja przypisania w powyższym przykładzie powinna mieć postać:
x = (double)a / b;
Instrukcja warunkowa
Instrukcja warunkowa pozwala na wykonanie lub zaniechanie wykonania pewnych czynności, w zależności od
konkretnego warunku logicznego. Instrukcja ma następującą składnię:
if(wyrażenie) instrukcja;
Wykonanie instrukcji warunkowej polega na wyliczeniu wartości wyrażenia i porównaniu tej wartości z zerem.
Jeżeli wartość ta jest różna od zera, to wykonana zostanie podana instrukcja, np.
if(x<0) printf("Ujemna wartość");
if(x==0) printf("Liczba zero");
if(x>0) printf("Dodatnia wartość");
Z trzech funkcji printf wykonana zostanie tylko jedna, w zależności od wartości zmiennej x.
printf("Podaj liczbę rzeczywistą różną od zera = ");
scanf("%lf", &x);
if(x==0) return;
Powyższy fragment kodu wczyta do zmiennej x liczbę typu double a następnie jeśli podana liczba okaże się
zerem zostanie wykonana instrukcja return, która zakończy wykonywanie funkcji głównej programu main.
Rozważmy dwie instrukcje:
if( wyrażenie) instrukcja1;
if(!wyrażenie) instrukcja2;
Z dwóch powyższych instrukcji wykonana zostanie tylko jedna: jeśli wyrażenie ma wartość różną od zera
program wykona instrukcję1, jeśli wyrażenie ma wartość równą zero, czyli !wyrażenie ma wówczas wartość
równą 1, program wykona instrukcję2.
Parę dwóch instrukcji warunkowych w powyższej postaci wykonuje się przy pomocy rozbudowanej formy
instukcji warunkowej:
if(warunek) instrukcja1;
else instrukcja2;
Słowo else oznacza "w przeciwnym wypadku". Jeżeli podane wyrażenie ma wartość różną od zera program
wykona instrukcję1, w przeciwny wypadku, tj. gdy wyrażenie ma wartość zero, program wykona instrukcję2.
Uwaga:
Po instrukcji występującej po słowie if wymagany jest zawsze średnik - odmiennie niż ma to miejsce w języku
Pascal.
Podana po słowach if lub else instrukcja może być pojedyńczą instrukcją lub blokiem instrukcji ujetym w
nawiasy klamrowe, np.
if(x!=0 && y>0)
{
z=y*(1/x);
t=++z-1;
}
else z=t=0;
Jeśli wyrażenie ma wartość różną od zera wykonany zostanie blok, w przeciwnym wypadku jedna instrukcja po
słowie else.
Ważne:
Po nawiasie zamykającym blok po słowie if nie umieszcza się średnika - może on natomiast wystąpić po bloku
występującym po słowie else, aczkolwiek nie jest wówczas konieczny.
Przykład
Dane są liczby rzeczywiste a, b i c będące współczynnikami trójmianu kwadratowego y=ax2+bx+c. Napisz
program, który wyznaczy pierwiastki tego trójmianu.
#include<stdio.h>
#include<math.h>
void main(void)
{
long double a, b, c;
long double delta, x1, x2;
//Wczytaj pierwszy współczynnik a i jeśli jest równy zero zakończ program
printf("\n\nPodaj współczynnik a trójmianu = ");
scanf("%Lf", &a);
if(a==0)
{
printf("Współczynnik a nie może być równy zero!");
return;
}
//Wczytaj pozostałe współczynniki b i c
printf("Podaj współczynnik b trójmianu = ");
scanf("%Lf", &b);
printf("Podaj współczynnik c trójmianu = ");
scanf("%Lf", &c);
//Oblicz wyróżnik trójmianu
delta=b*b-4*a*c;
//Jeśli dodatni oblicz dwa pierwiastki i wypisz je
if(delta>0)
{
x1=(-b-sqrt(delta))/(2*a);
x2=(-b+sqrt(delta))/(2*a);
printf("Trójmian ma dwa pierwiastki rzeczywiste x1 i x2 :\n");
printf(" x1 = %.3Lf\n x2 = %.3Lf\n", x1, x2);
}
else //W przeciwnym wypadku sprawdź czy jest równy zero
if(delta==0)
{
//Oblicz pierwiastek podwójny i wypisz go
x1=-b/(2*a);
printf("Trójmian ma jeden pierwiastek rzeczywisty podwójny x0 :\n");
printf(" x0 = %.3Lf\n", x1);
}
else //W przeciwnym wypadku delta jest ujemna
printf("Trójmian nie posiada pierwiastków rzeczywistych.");
}
Po obliczeniu wyróżnika delta program zawiera już tylko jedną instrukcję warunkową if w postaci
rozbudowanej: instrucją po słowie if jest blok, natomiast po słowie else instrukcją jest kolejna instrukcja
warunkowa if, również w postaci rozbudowanej.
Przy obliczaniu wartości pierwiastów x1 i x2 wywoływana jest standardowa funkcja sqrt zdefiniowana w
module math.h, której wartością jest pierwiastek kwadratowy.
Instrukcja iteracyjna for
Instrukcje iteracyjne (zwane pętlami) służą do wielokrotnego wykonywania podobnych do czynności. Pętle
języków programowania dzieli się dzieli na:
• pętle o ustalonej liczbie powtórzeń (iteracji),
• pętle o nieustalonej liczbie powtórzeń (iteracji).
Pętla for jest przykładem pętli o ustalonej liczbie powtórzeń i posiada następującą składnię:
for(instrukcja_początkowa; wyrażenie1; wyrażenie2) instrukcja;
na przykład:
for(x=0; x<100; x++) printf("\n%10lu", x*x);
Powyższa instrukcja spowoduje wypisanie na ekranie kwadratów wszystkich liczb naturalnych z zakresu 0..999,
każdej w osobnej linii na dziesieciu komórkach ekranu.
Pierwsza instrukcja o nazwie instrukcja_początkowa wykonywana jest zawsze przed rozpoczęciem
wykonywania pętli(w powyższym przykładzie zmienna x otrzyma wartość 10. Wykonanie pętli składa sie trzech
kroków powtarzanych sekwencyjnie:
1. zostanie obliczone wyrażenie1 zwane wyrażeniem warunkowym i porównane z liczbą zero. Jeśli
wartość tego wyrażenia będzie równa zero, wykonanie instrukcji kończy się, w przeciwnym wypadku
następuje przejście do pkt. 2,
2. Zostanie wykonana podana instrukcja i nastąpi przejście do pkt. 3,
3. Zostanie obliczone wyrażenie2 zwane wyrażeniem zwiększającym i nastąpi przejście do pkt. 1.
Z powyższego schematu wynika, że zakończenie pętli może nastąpić tylko w pkt. 1, przy porównywaniu
wyrażenia1 z liczbą zero: pętla kończy się jeśli wyrażenie to ma wartość zero, inaczej mówiąc, jeśli osiągnie
wartość fałszu.
W przytoczonym przykładzie każde wykonanie wyrażenia zwiększającego powoduje zwiększenie wartości
zmiennej x o jeden. Pętla będzie kontynuowana do momentu, aż zmienna x osiągnie wartość 100 - wówczas
porównanie x<100 będzie fałszywe i nastąpi zakończenie pętli.
Przykład
Dane są: liczba rzeczywista p oraz nieujemna liczba całkowita n. Napisz program, który wyznaczy potęgę pn.
#include<stdio.h>
long double potega(long double podstawa, unsigned int wykladnik)
{
long double wynik=1;
unsigned int i;
for(i=1; i<=wykladnik; i++) wynik*=podstawa;
return wynik;
}
void main(void)
{
long double p;
unsigned int n;
printf("Podaj podstawę potęgi p = ");
scanf("%Lf", &p);
printf("Podaj nieujemny wykładnik potęgi n = ");
scanf("%u", &n);
printf("\nWyznaczona potęga: p^n = %Lf\n", potega(p, n));
}
W powyższym programie zdefiniowana została funkcja o nazwie potega, dwóch argumentach typów
odpowiednio long double i unsigned int oraz wartościach typu long double.
Zwrócona przez tą funkcję wartość nie wynika rzecz jasna z jej nazwy, tylko z umieszczonego w ciele funkcji
algorytmu.
Jeśli potęgę 58 zapiszemy w postaci iloczynu 5*5*5*5*5*5*5*5, to dojdziemy do wniosku, że jej obliczenie
wymaga ośmiokrotnego pomnożenia liczby 1 przez 5 (kolejno: 1*5, 5*5, 25*5, 125*5, 625*5, 3125*5, 15625*5
i 78125*5 = 390625, każda z wymienionych liczb jest kolejną potęgą liczby 5). Ten sam algorytm (metodę)
można zastosować dla innej podstawy niż 5 i innego wykładnika niż 8. Metoda zatem jest uniwersalna i będzie
skuteczna dla wszystkich możliwych wartości zarówno podstawy jak i wykładnika - z jednym wyjątkiem rzecz
jasna, a mianowicie wykładnika równego zero.
Funkcja potega wyznacza potęgę w taki właśnie sposób, mnożąc wynik (początkowo równy jeden) przez
podstawę potęgi podstawa tyle razy, ile wynosi wykladnik: po pierwszym wykonaniu pętli zmienna wynik
otrzyma wartość 1*podstawa, po drugim wykonaniu wartość 1*podstawa*podstawa=podstawa2, po trzecim
wartość 1*podstawa*podstawa*podstawa=podstawa3, itd.
Po zakończeniu pętli zmienna wynik będzie równa podstawawykladnik.
A wyjątek...
Wystarczy zwrócić uwagę na zainicjowanie zmiennej wynik liczbą 1 oraz warunek pętli i<wykladnik: jeśli
wykładnik będzie równy zero, to pętla zakończy się bez żadnego przebiegu (wykonania podanej instrukcji) i
funkcja jako wartość zwróci wynik, czyli liczbę 1.
W ciele funkcji głównej main wczytywane są jedynie wartości p i n, wypisanie wyniku funkcją printf następuje
poprzez wywołanie zdefiniowanej funkcji potega, która zwróci oczekiwaną potegę pn.
Jeżeli przeanalizujemy działanie funkcji potega dla argumentow p=0 i n=0 to dojdziemy do wniosku, że wynik
również będzie jeden, co nie do końca jest zgodne z prawami matematyki.
Warto również zwrócić uwagę na typ wykładnika unsigned int. Jeżeli po uruchomieniu tego programu jako
wykładnik wpiszemy liczbę ujemną, np. -12, to funkcja scanf odczyta ją jako dodatnią dzięki automatycznej
konwersji z typu signed int do typu unsigned int (będzie to liczba 65524). Być może programista chce w takim
przypadku zwrócić użytkownikowi uwagę, na błędnie wprowadzoną wartość wykładnika - należy wówczas
wczytać wykładnik do zmiennej signed int a następnie sprawdzić istrukcją warunkową if, czy nie jest ujemny.