prof. Jan Bielecki
Visual C++ 6.0
Podstawy programowania
1.
Pierwsze kroki
2.
Ś
rodowisko Visual C++
3.
Wskaźniki i odnośniki
4.
Przetwarzanie łańcuchów
5.
Posługiwanie się funkcjami
6.
Zarządzanie pamięcią
7.
Widoczność deklaracji
8.
Studia programowe
Dodatki
Priorytety operatorów
Opracowywanie wyraŜeń
Konwersje standardowe
Operatory bitowe i warunkowe
Operacje wejścia-wyjścia
1
Pierwsze kroki
Program jest zbiorem modułów źródłowych. KaŜdy moduł składa się z deklaracji typów, zmiennych i funkcji.
Napis od znaków // do końca wiersza jest komentarzem. Jako taki nie ma wpływu na przebieg wykonania
programu.
Dokładnie jeden moduł, nazywany głównym, zawiera deklarację funkcji main. Wykonanie programu polega na
opracowaniu wszystkich jego globalnych deklaracji, a następnie przystąpieniu do wykonywania instrukcji
zawartych w funkcji main. Zakończenie wykonywania programu następuje po wykonaniu w funkcji głównej
instrukcji return, albo tuŜ po powrocie z funkcji exit. MoŜe to nastąpić jeszcze przed podjęciem wykonywania
funkcji głównej.
int main(void) // deklaracja funkcji głównej
{
return 0; // instrukcja return
}
void exit(int); // deklaracja funkcji exit
struct Empty { // deklaracja typu Empty
Empty(void)
{
exit(0); // wywołanie funkcji exit
}
};
Empty obj; // deklaracja zmiennej
Program napisano w taki sposób, aby jego wykonanie zakończyło się przed podjęciem wykonywania funkcji
głównej.
Komunikacja z otoczeniem
W najprostszym przypadku, program pobiera dane z klawiatury i wyprowadza je na monitor. Operacje
wprowadzania danych odbywają się za pomocą operatora >>, a operacje wyprowadzania danych za pomocą
operatora <<. Klawiatura jest reprezentowana przez zmienną cin, a monitor przez zmienną cout. PosłuŜenie się
nimi wymaga uŜycia dyrektywy #include wyszczególniającej nazwę iostream.h.
Daną wprowadzoną z klawiatury kończy odstęp, uzyskany przez naciśnięcie klawisza Space, Tab albo Enter.
Analiza danych wejściowych następuje wierszami, to jest dopiero po naciśnięciu klawisza Enter. W
szczególności, jeśli program oczekuje 3 danych, to kaŜdą moŜna podać w osobnym wierszu, albo wszystkie
podać w jednym wierszu. Przed wprowadzeniem kolejnej danej pomija się poprzedzające ją odstępy.
Uwaga: Wygodnym sposobem wyprowadzenia odstępu Enter jest uŜycie symbolu endl, a wygodnym sposobem
wyprowadzenia znaku o kodzie 0 jest uŜycie symbolu ends.
PoniewaŜ operacja wejścia-wyjścia dostarcza w miejscu jej uŜycia jej lewy argument, więc zapis pary instrukcji
cin >> one;
cin >> two;
moŜna uprościć do
2
cin >> one >> two;
Właściwość tę, nazywaną łączeniem operacji, moŜna zastosować takŜe do wyprowadzania danych.
#include <iostream.h>
int main(void)
{
int one, two;
cout << "Enter 2 numbers:" << endl;
cin >> one >> two;
cout << "Sum = " << one + two << endl;
return 0;
}
Program wyprowadza zachętę do wprowadzenia 2 liczb, a następnie wyznacza i wyprowadza ich sumę.
Wykonywanie operacji
Wykonanie programu sprowadza się do wykonania operacji na danych. W Dodatku A zamieszczono kompletny
wykaz operacji, a w Dodatku B omówiono zasady opracowywania wyraŜeń. Podane tam opisy stanowią istotny
element niniejszego opracowania.
Operacje przypisania
Prosta operacja przypisania ma postać
a = b
w której a i b są wyraŜeniami, ale ponadto a jest l-nazwą zmiennej (por. Dodatek B).
Wykonanie operacji polega na przypisaniu zmiennej a wartości wyraŜenia b.
ZłoŜona operacja przypisania ma postać
a
@
= b
w której @= jest jednym z operatorów wymienionych w Dodatku A (np. +=, -=, *= , /=).
Operację a @= b (np. a += b) wykonuje tak, jak operację
a = a + b
ale wymaga się, aby opracowanie a i b było jednokrotne.
Operacja połączenia
Operacja połączenia ma postać
a , b
Jej wykonanie składa się z opracowania wyraŜenia a (wyłącznie dla jego skutków ubocznych) oraz z niejawnego
zastąpienia całej operacji nazwą zmiennej reprezentowanej przez wyraŜenie b.
3
Uwaga: Nie jest operatorem połączenia przecinek oddzielający parametry i argumenty funkcji.
W szczególności wykonanie instrukcji
return a = 10, cout << a, b = 20;
jest równowaŜne wykonaniu instrukcji
a = 10;
cout << a;
b = 20;
return b;
Operacje arytmetyczne
Operacje arytmetyczne wykonuje się za pomocą operatorów wymienionych w tabeli Operacje arytmetyczne.
Tabela Operacje arytmetyczne
###
+
(dodawanie)
-
(odejmowanie)
*
(mnoŜenie)
/
(dzielenie)
%
(reszta)
++ (zwiększenie o 1)
--
(zmniejszenie o 1)
+= (dodanie)
-=
(odjęcie)
*= (pomnoŜenie)
/=
(podzielenie)
###
Sposób wykonania podstawowych działań arytmetycznych nie wymaga opisu. NaleŜy jedynie zauwaŜyć, Ŝe
rezultat dzielenia całkowitego jest całkowity, a argumenty wyznaczania reszty muszą być całkowite (np. 11 / 4
ma wartość 2, a 11 % 4 ma wartość 3).
Operacje przedrostkowe
Wykonanie operacji ++num powoduje zwiększenie wartości zmiennej num o 1. W miejsce wykonania operacji
jest dostarczana nowa wartość num.
Wykonanie operacji --num powoduje zmniejszenie wartości zmiennej num o 1. W miejsce wykonania operacji
jest dostarczana nowa wartość num.
int fix = 10;
cout << ++fix; // 11
cout << fix; // 11
Operacje przyrostkowe
Wykonanie operacji num++ powoduje zwiększenie wartości zmiennej num o 1. W miejsce wykonania operacji
jest dostarczana pierwotna wartość num.
Wykonanie operacji num-- powoduje zmniejszenie wartości zmiennej num o 1 W miejsce wykonania operacji
jest dostarczana pierwotną wartość num.
int fix = 10;
cout << fix--; // 10
cout << fix; // 9
4
Operacje porównania
Operacje porównania wykonuje się za pomocą operatorów wymienionych w tabeli Operacje porównania.
Tabela Operacje porównania
###
==
(równe)
!=
(nie równe),
<
(mniejsze)
>
(większe),
<=
(mniejsze lub równe)
>=
(większe lub równe)
###
Jeśli porównanie wyraŜa orzeczenie prawdziwe, to jego rezultat ma wartość true (prawda). W przeciwnym razie
ma wartość false (fałsz).
Uwaga: Porównanie na równość wykonuje się za pomocą operacji ==, a nie za pomocą operacji =. Zaniedbanie
tego faktu jest źródłem trudnych do wykrycia błędów semantycznych.
#include <iostream.h>
int main(void)
{
int num = 0;
while(num == 0)
cin >> num;
cout << num << endl;
return 0;
}
Program wyprowadza liczbę 0 albo pierwszą niezerową liczbę wprowadzoną z klawiatury. Gdyby operator
porównania zastąpiono operatorem przypisania, to zawsze wyprowadzałby liczbę 0.
Operacje orzecznikowe
Operacje orzecznikowe wykonuje się za pomocą operatorów wymienionych w tabeli Operacje orzecznikowe.
Tabela Operacje orzecznikowe
###
!
(zaprzeczenie)
&&
(koniunkcja)
||
(dysjunkcja)
###
Argumenty i rezultaty operacji orzecznikowych są typu bool i mają wartości true albo false.
Rezultat zaprzeczenia ma wartość true tylko wówczas, gdy argument ma wartość false.
Rezultat koniunkcji ma wartość true tylko wówczas, gdy oba argumenty mają wartość true.
Rezultat dysjunkcji ma wartość false tylko wówczas, gdy oba argumenty mają wartość false.
Uwaga: Operacja koniunkcji i dysjunkcji jest wykonywana w taki sposób, Ŝe jeśli po opracowaniu pierwszego
argumentu jest znana wartość rezultatu całej operacji (bo dla koniunkcji ma wartość false, a dla dysjunkcji ma
wartość true), to rezygnuje się z opracowania drugiego argumentu.
#include <iostream.h>
int vec[] = { 10, 20, 30, 40, 50 };
int main(void)
{
5
int pos;
cin >> pos;
pos >= 0 && pos < 5 && (cout << vec[pos]);
return 0;
}
Program wyprowadza wartość tego elementu tablicy, którego indeks wprowadzono z klawiatury.
Jeśli wprowadzi się indeks, który nie ma wartości z przedziału [0 ; 4], to program nie wyprowadzi nic.
Dzięki uŜyciu operatora &&, nigdy nie dojdzie do opracowania wyraŜenia vec[pos] z niedozwolonym indeksem.
Operacje konwersji
Wykonanie konwersji ma na celu przekształcenie zmiennej pewnego typu w zmienną typu docelowego.
Operacja konwersji wyraŜenia e do typu Type ma postać
(Type)e
Jeśli nazwę typu docelowego Type moŜna wyrazić za pomocą identyfikatora (np. int), to operację konwersji
moŜna zapisać jako
Type(e)
W szczególności, jeśli w programie występuje instrukcja
int num = 4.8;
w której zmienna num jest typu int, a wyraŜenie 4.8 jest typu double, to poniewaŜ danej typu double (zazwyczaj
8-bajtowej) nie moŜna pomieścić w zmiennej typu int (zazwyczaj 4-bajtowej), więc najprościej byłoby taką
instrukcje uznać za błędną.
PoniewaŜ w C++ przekształcenie zmiennej typu double w zmienną typu int zdefiniowano jako konwersję
standardową (polega ona na odrzuceniu części ułamkowej), więc rozpatrywana instrukcja zostanie niejawnie
zmieniona w poprawną instrukcję
int num = int(4.8);
równowaŜną
int num = 4;
w której wyraŜenie inicjujące jest juŜ typu int.
Uwaga: WaŜne informacje na temat konwersji zamieszczono w Dodatku C.
Operacje warunkowe
Operacje warunkowe wykonuje się za pomocą trójargumentowego operatora ?: (pytajnik, dwukropek).
Rezultatem operacji
e ? eT : eL
6
jest zmienna o wartości eT jeśli orzeczenie wyraŜone przez e jest prawdziwe, albo zmienna o wartości eF w
przeciwnym razie..
Uwaga: Po opracowaniu wyraŜenia e, opracowuje się tylko jedno z wyraŜeń eT i eF.
num = fix > 0 ? fix1 : fix2;
num < 0 ? fix1 : fix2 = 30;
Operatory :: i Name::
Jeśli id jest identyfikatorem, to ::id jest nazwą globalną, a Name::id jest nazwą składnika typu strukturowego
Name.
int num = 0;
struct Fix {
int num;
void set(int num =::num)
{
Fix::num = num;
}
};
Napis ::num jest nazwą zmiennej globalnej, a napis Fix::num jest nazwą składnika num.
Prawy argument przypisania Fix::num = num jest nazwą parametru.
Wykonywanie instrukcji
Do napisania dowolnego programu wystarczy zaledwie kilka instrukcji. NajwaŜniejszymi z nich są: instrukcja
pusta, grupująca, warunkowa (if), iteracyjna (while) i powrotu (return). Opis pozostałych ograniczono do
przykładów.
Instrukcja pusta
Instrukcja pusta składa się ze średnika.
;
Jej wykonanie nie ma Ŝadnych skutków.
Instrukcja grupująca
Instrukcja grupująca składa się z nawiasów klamrowych zawierających dowolną sekwencję instrukcji.
Jeśli w miejscu, w którym składnia wymaga uŜycia dokładnie jednej instrukcji, chce się umieścić ich więcej, to
wystarczy ująć je w nawiasy klamrowe i powstanie jedna instrukcja.
{ int a; cin >> a; a++; cout << a; }
Instrukcja warunkowa
Instrukcja warunkowa ma postać
7
if(c)
s
albo
if(c)
s1
else
s2
w której c jest wyraŜeniem orzecznikowym o wartości true albo false, a s1 oraz s2 jest pojedynczą instrukcją
(np. instrukcją grupującą).
Wykonanie instrukcji warunkowej zaczyna się od opracowania wyraŜenia c (np. a > 2). Jeśli wyraŜone przez nie
orzeczenie jest prawdziwe, to w pierwszym przypadku jest wykonywana instrukcja s, a w drugim instrukcja s1.
W przeciwnym razie, w pierwszym przypadku nie robi się nic, a w drugim wykonuje instrukcję s2.
if(a > 2)
{ a++; cout << a; }
else
{ cout << a; a-- }
albo równowaŜnie
if(a > 2) {
a++;
cout << a;
} else {
cout << a;
a--;
}
Jeśli podczas opracowywania instrukcji warunkowej napotka się słowo kluczowe else, to przyjmuje się, Ŝe
dotyczy ono najbliŜszego z lewej słowa if, nie połączonego jeszcze z else.
W szczególności instrukcja
if(fix1 > fix2) if(fix1) fix1++; else fix2++;
jest wykonywana jak instrukcja
if(fix1 > fix2) {
if(fix1)
fix1++;
else fix2++;
}
a nie jak instrukcja
if(fix1 > fix2) {
if(fix1)
fix1++;
} else
fix2++;
Instrukcje iteracyjne
Instrukcja iteracyjna while ma postać
while(c)
s
8
w której c jest wyraŜeniem orzecznikowym, a s jest pojedynczą instrukcją.
Wykonanie instrukcji iteracyjnej while polega na cyklicznym badaniu orzeczenia wyraŜonego przez wyraŜenie c
i wykonywaniu instrukcji s.
Iteracja kończy się w chwili stwierdzenia, Ŝe orzeczenie jest nieprawdziwe. Jeśli okaŜe się to juŜ na wstępie, to
instrukcja s nie będzie wykonana wcale.
int i = 3;
while(i > 0) {
int t = i * i;
cout << t << endl; // 9 4 1
i--;
}
Często uŜywa się instrukcji iteracyjnej for
for(d c ; e) {
s s ... s
}
w której d jest instrukcją deklaracyjną, a c i e są wyraŜeniami.
Tak zapisana instrukcja for jest równowaŜna instrukcji
d
while(c) {
s s ... s
e;
}
Instrukcja for dobrze opisuje czynności o znanej liczbie powtórzeń.
int tab[5] = { 10, 20, 30, 40, 50 }, sum = 0;
for(int i = 0; i < 5 ; i++)
sum += tab[i];
cout << "Sum = " << sum << endl;
Instrukcja zaniechania
Instrukcja zaniechania ma postać
break;
Wykonanie instrukcji zaniechania powoduje zakończenie wykonywania najwęŜszej obejmującej ją instrukcji
iteracyjnej albo decyzyjnej.
int sum = 0;
while(true) {
int tmp = 0;
cin >> tmp; // wprowad
ź
dan
ą
if(tmp == 0) // zbadaj czy 0
break; // zako
ń
cz iteracj
ę
sum += tmp; // dosumuj
}
cout << "Sum = " << sum << endl;
albo
int tmp = 0, sum = 0;
while(cin >> tmp, tmp) // wprowad
ź
i zbadaj
sum += tmp; // dosumuj
cout << "Sum = " << sum << endl;
9
lub
for(int tmp = 0, sum = 0; cin >> tmp, tmp ; sum += tmp);
cout << "Sum = " << sum << endl;
Instrukcja powrotu
Instrukcja powrotu ma postać
return e;
w której e jest wyraŜeniem.
Wykonanie instrukcji powrotu powoduje zakończenie wykonywania funkcji i dostarczenie rezultatu o wartości
określonej przez e.
int sum(int one, int two)
{
return one + two;
}
Jeśli typem funkcji jest void, to uŜyta w niej instrukcja powrotu nie moŜe zawierać wyraŜenia. UŜycie takiej
instrukcji jest zazwyczaj zbyteczne, poniewaŜ domniemywa się ją tuŜ przed klamrą zamykajacą ciało funkcji.
void sum(int one, int two)
{
cout << one + two << endl;
return; // zb
ę
dne
}
Instrukcja decyzyjna
Instrukcja decyzyjna uogólnia instrukcję warunkową i jest przydatna wówczas, gdy w programie występują
więcej niŜ dwie gałęzie decyzyjne. W szczególności instrukcję warunkową
if(a == 2)
b = 3;
else if(a == 1)
b = 5;
else if(a == 4)
b = -1;
else
b = 0;
moŜna zapisać w postaci
switch(a) {
case 2: // je
ś
li a == 2
b = 3;
break;
case 1: // je
ś
li a == 1
b = 5;
break;
case 4: // je
ś
li a == 4
b = -1;
break;
default: // w pozostałych przypadkach
b = 0;
}
10
Deklarowanie zmiennych i typów
KaŜdy moduł programu jest kompilowany niezaleŜnie od pozostałych. Analiza składniowa modułu odbywa się
od-góry-do-dołu i od-lewej-do-prawej i polega na rozpoznawaniu jednostek leksykalnych: identyfikatorów
(np. exit), literałów (np. 0), operatorów (np. +) i ograniczników (np. ;).
Identyfikatory
Identyfikatorem jest spójna sekwencja liter i cyfr, zaczynająca się od litery. Identyfikator nie moŜe mieć postaci
słowa kluczowego (np. return). Za jego literę uznaje się równieŜ znak podkreślenia (_).
Litery małe uznaje się za róŜne od duŜych. Zaleca się, aby w wielosłowowych nazwach zmiennych i funkcji,
wszystkie słowa, z wyjątkiem pierwszego, były zapisane za pomocą duŜych liter.
np.
forSale speedLimit veryLongName
Literały
Literałami są liczby (np. 12, 0xff i 2.e-3), znaki (np. 'a') i łańcuchy (np. "Hello"). KaŜdy literał jest nazwą
zmiennej ustalonej. Jej typ wynika z zapisu literału.
Uwaga: Jeśli łańcuch ma zawierać znak \ (ukośnik), to naleŜy go zapisać jako \\ (np. "C:\\Data.txt).
np.
'a'
'\n'
'\0' // nazwy zmiennych typu char
12
-12
0
// nazwy zmiennych typu int
-2.4
2.e4
.2 // nazwy zmiennych typu double
"a"
"N"
"\n" // nazwy zmiennych typu char [2]
Deklaracje
KaŜde uŜycie identyfikatora musi być poprzedzone jego deklaracją. Deklaracja kompletnie opisująca zmienną
(określająca jej wartość początkową), typ (wyszczególniająca strukturę jego obiektów) i funkcję (podająca jej
ciało) jest nazywana definicją.
W skład deklaracji wchodzą specyfikatory, deklaratory i inicjatory.
np.
const int tab[3] = { -1, 0, +1 };
Specyfikatorami są const i int, deklaratorem jest tab[3], a inicjatorem jest = { -1, 0, +1 }.
Nagłówki
Deklaracje typów i funkcji są ujmowane w nagłówki. KaŜdy nagłówek jest zapisany w odrębnym pliku.
Włączenie nagłówka odbywa się w miejscu wystąpienia wyszczególniającej go dyrektywy #include.
Do najczęściej uŜywanych nagłówków naleŜą: iostream.h i iomanip.h, math.h, string.h i stdlib.h. Dwa
pierwsze włączają do modułu deklaracje zmiennych i operatorów wejścia-wyjścia (cin, cout, >>, <<), dwa
następne włączają deklaracje funkcji matematycznych (sqrt, sin, cos) i łańcuchowych (strlen, strcpy, strcat,
strcmp), a ostatni włącza m.in. deklarację funkcji exit.
11
#include <iostream.h>
#include <math.h>
int main(void)
{
double number; // deklaracja zmiennej
cin >> number; // wprowadzenie liczby
cout << sqrt(number); // wyprowadzenie pierwiastka
return 0; // zako
ń
czenie wykonywania
}
Zmienne
Zmienną jest obszar pamięci do przechowywania danych określonego typu: skalarnych, tablicowych i
strukturowych. KaŜde odwołanie do zmiennej musi być poprzedzone deklaracją jej typu.
int fix; // zmienna całkowita
char chr; // zmienna znakowa
double num; // zmienna rzeczywista
Zmienna fix jest typu int, zmienna chr jest typu char, zmienna num jest typu double.
Rozmiar zmiennej
Rozmiar zmiennej w bajtach określa się za pomocą operatora sizeof. Argumentem operatora sizeof moŜe być
nazwa zmiennej albo nazwa typu.
Uwaga: Rozmiar zmiennej zaleŜy od implementacji. W Visual C++ zmienne typu char są 1-bajtowe, zmienne
typu int są 2-bajtowe, a zmienne typu double są 8-bajtowe.
int age = 24;
cout << sizeof(age); // 4
cout << sizeof(int); // 4
int tab[3];
cout << sizeof(tab); // 12
Zmienne ustalone
Zmienna zadeklarowana ze specyfikatorem const jest zmienną ustaloną. Zmienna ustalona musi być
zainicjowana, ale nadana jej wartość nie moŜe ulec zmianie.
Uwaga: Zmiennymi ustalonymi są takŜe zmienne reprezentowane przez literały. W szczególności liczba 12e2
jest nazwą zmiennej ustalonej o wartości 1200.
const int size = 100;
const double width = -2e-7, height = 2e2;
const int tab[2] = { 10, 20 };
Zmienne skalarne
Deklaracja zmiennej skalarnej określa jej identyfikator oraz wyszczególnia typ danych jakie moŜna przypisywać
zmiennej (np. int, double, char).
int number;
double speedLimit;
char separator;
12
Wartość początkową zmiennej określa się za pomocą inicjatora. Jeśli deklaracja zmiennej zawiera jawny albo
domniemany inicjator, to jest jej definicją.
int minValue = 10, maxValue = 90;
double width = 2.4, height = 4.5e+2, area;
char lastChar = '.';
Składnia inicjatora
Inicjatory dzielą się na wyraŜeniowe, klamrowe i nawiasowe. Inicjator zmiennej ustalonej musi mieć postać
wyraŜenia stałego. W jego skład wchodzą odwołania do literałów i zmiennych ustalonych, ale nie mogą
wchodzić odwołania do zmiennych nie-ustalonych.
int base = 100; // inicjator wyra
Ŝ
eniowy
int min = { base + 20 }; // inicjator klamrowy
int max(base + 40); // inicjator nawiasowy
const int size = max - min; // bł
ą
d
Punkt zadeklarowania
Identyfikator zmiennej uwaŜa się za zadeklarowany w punkcie tuŜ przed inicjatorem wyraŜeniowym i
klamrowym, ale tuŜ po inicjatorze nawiasowym. Ta subtelna róŜnica ma niekiedy wpływ na poprawność i skutek
wykonania programu.
#include <iostream.h>
const int val = 10; // definicja zmiennej globalnej
int main(void)
{
int val(val); // definicja zmiennej lokalnej
cout << val; // 10
return 0;
}
Punkt zadeklarowania zmiennej lokalnej występuje tuŜ po inicjatorze (val). Gdyby inicjator nawiasowy
zastąpiono jednym z pozostałych inicjatorów, to program stałby się błędny, poniewaŜ zmienna lokalna byłaby
wówczas inicjowana nie wartością zmiennej globalnej, ale nieokreśloną jeszcze wartością zmiennej lokalnej.
Operacje wejścia-wyjścia
Zmienne typu int, double i char są zmiennymi arytmetycznymi, przystosowanymi odpowiednio do
przechowywania liczb całkowitych, zmiennopozycyjnych i kodów znaków.
Podczas wykonywania operacji wejścia, do zmiennych typu int i double wprowadza się dane liczbowe, a do
zmiennych typu char wprowadza się kody znaków. A zatem, jeśli z klawiatury wprowadzi się na przykład napis
20e3, to liczba pobranych znaków i otrzymana wartość będzie zaleŜeć od typu zmiennej, zgodnie z tabelą
Wprowadzanie danych.
Tabela Wprowadzanie danych
Typ zmiennej
Pobrano znaków
Wprowadzono wartość
int
2
20
double
4
20000
char
1
49
13
Podczas wykonywania operacji wyjścia, wyprowadza się liczby o wartości zmiennych typu int i double oraz
znaki o kodach określonych przez wartości zmiennych typu char.
#include <iostream.h>
int main(void)
{
int mant, exp;
char sep;
cin >> mant >> sep >> exp;
int value = mant;
while(exp > 0) {
value = value * 10;
exp--;
}
cout << mant << sep << exp <<
" == " << value << endl;
return 0;
}
Jeśli wprowadzi się napis 2e3, to program wyprowadzi ten napis oraz liczbę 2000.
Zmienne tablicowe
Zmienną tablicową, w skrócie tablicą, jest zestaw sąsiadujących ze sobą elementów tablicy. KaŜdy element jest
zmienną takiego samego typu: skalarną, tablicową, strukturową.
int tab[20];
Zmienna tab jest tablicą o 20-elementach typu int.
Z kaŜdym elementem tablicy jest związany indeks, określający połoŜenie elementu w obrębie tablicy. Elementy
tablicy są indeksowane od 0. W deklaracji tablicy podaje się liczbę jej elementów, a nie indeks jej ostatniego
elementu. Jeśli deklarator nie podaje liczby elementów, ale deklaracja zawiera inicjator, to za liczbę elementów
uznaje się liczbę fraz inicjujących.
Uwaga: Liczba fraz inicjujących nie moŜe przekraczać liczby elementów tablicy. Jesli jest od niej mniejsza, to
jest niejawnie dopełniana frazami 0.
int tab[100] = { 4, 4 };
Zerowy i pierwszy element tablicy tab ma wartość 4. Wszystkie pozostałe mają wartość 0.
Liczba elementów tablicy musi być wyraŜona za pomocą wyraŜenia stałego. WyraŜenie stałe moŜe zawierać
literały i identyfikatory zmiennych ustalonych, ale nie moŜe zawierać operatora połączenia.
const int Count = 3;
double sizes[Count] = { 2.4, 3.8, 5.2 };
int values[] = { 10, 20, 30, 40, 50 };
int Size = 4;
double reals[Size]; // bł
ą
d
Tablica sizes składa się z 3 zmiennych, z których kaŜda jest typu double.
Tablica values składa się z 5 zmiennych, z których kaŜda jest typu int.
Identyfikowanie elementów tablicy
14
Jeśli nazwą tablicy jest vec, to nazwą jej elementu o indeksie ind jest vec[ind]. Jest to prawdziwe tylko wówczas,
gdy wyraŜenie ind ma wartość większą-lub-równą 0 i jednocześnie mniejszą od liczby elementów tablicy.
Uwaga: Jeśli tablica vec ma n elementów, to zezwala się, aby wyraŜenie ind miało wartość -1 oraz n, ale tylko
wówczas, gdy opracowanie wyraŜenia vec[ind] nie ma na celu dokonania zmiany albo dostarczenia wartości
elementu.
#include <iostream.h>
int values[5] = { 10, 20, 30, 40, 50 };
int main(void)
{
int index;
cin >> index;
if(index >= 0 && index < 5)
cout << values[index] << endl;
else
cout << "Wrong index" << endl;
return 0;
}
Program wyprowadza wartość elementu o podanym indeksie. Jeśli indeks nie mieści się w domkniętym
przedziale [0 ; 4], to program wyprowadza napis Wrong index.
Tablice znakowe
Tablicą znakową jest tablica o elementach typu char. Przechowuje się w niej zazwyczaj małe liczby oraz kody
znaków.
PoniewaŜ Visual C++ uŜywa kodu ASCII, w którym kodem cyfry 0 jest 48, więc zainicjowanie 4-elementowej
tablicy znakowej kodami cyfr 0, 1 i 2 oraz kodem znaku '\0' moŜna wykonać na wiele sposobów, w tym m.in.
char digits[] = { '0', '1', '2', '\0' };
char digits[] = { 48, 49, 50, 0 };
char digits[4] = { '0', '0'+1, '3'-1 };
char digits[4] = "012";
Z klawiatury moŜna wprowadzać tylko spójne ciągi znaków. Za ostatnim wprowadzonym znakiem umieszcza się
wówczas specjalny znak o kodzie 0.
Jeśli chce się wyprowadzić ciąg znaków utworzony w tablicy programowo, to naleŜy zakończyć go znakiem o
kodzie 0 (jego rozpoznanie spowoduje zakończenie wyprowadzania znaków).
#include <iostream.h>
char name[100];
int main(void)
{
cin >> name;
name[1] = 0;
cout << "Your initial is: " << name << endl;
return 0;
}
Program wprowadza imię, a następnie wyprowadza jego inicjał.
Literały łańcuchowe
15
Literał łańcuchowy, na przykład "Hello", ma postać ciągu znaków ujętego w cudzysłowy. Znaki specjalne są w
tym ciągu reprezentowane przez nastepujące symbole
\\ (ukośnik)
\n (nowy wiersz)
\t (tabulator),
\' (apostrof)
\" (cudzysłów)
\0 (znak o kodzie 0).
KaŜdy literał łańcuchowy, jest nazwą tablicy o elementach typu char, zainicjowanych kodami kolejnych znaków
literału oraz kodem znaku \0. W szczególności (w kodzie ASCII) literał "No" jest nazwą 3-elementowej tablicy
zainicjowanej liczbami 78, 111 i 0.
#include <iostream.h>
int main(void)
{
int i = 0;
while("Hello"[i] != 0) {
cout << "Hello"[i] << ' ';
i++;
}
cout << endl;
return 0;
}
Program wyprowadza kolejne znaki napisu Hello, po kaŜdym znaku dodając spację. Zakończenie wykonywania
następuje po rozpoznaniu elementu zainicjowanego liczbą 0.
Literały łańcuchowe mogą być uŜyte do inicjowania tablic znakowych. Tak zainicjowana tablica musi mieć co
najmniej tyle elementów ile ma tablica reprezentowana przez literał. Jeśli jest dłuŜsza, to jej nadmiarowe
elementy są inicjowane liczbami 0.
char name1[10] = { 'I', 's', 'a', 0 };
char name2[10] = "Isa";
char name3[] = "Isa";
char name4[3] = "Isa"; // bł
ą
d
Operacje wejścia-wyjścia
Tablice znakowe mogą być wykorzystane do wprowadzania z klawiatury spójnych ciągów znaków. W takim
przypadku argumentem operacji jest zazwyczaj nazwa tablicy, a wykonanie operacji powoduje umieszczenie w
tablicy kodów znaków łańcucha oraz kodu o wartości 0.
PoniewaŜ moŜe wówczas dojść do przepełnienia tablicy, zaleca się uŜycie manipulatora setw, zadeklarowanego
w nagłówku iomanip.h, ograniczającego liczbę wprowadzonych znaków.
Uwaga: Manipulator setw moŜe być uŜyty takŜe podczas wyprowadzania danych. W takim wypadku określa on
szerokość pola zewnętrznego, w którym umieszcza się dane wyjściowe.
#include <iostream.h>
#include <iomanip.h>
char name[20];
int main(void)
{
cin >> setw(20) >> name;
name[1] = 0;
cout << "Your initial is: " << name << endl;
return 0;
}
16
Program wprowadza imię, a następnie wyprowadza jego inicjał. Aby zabezpieczyć się przed wpisaniem do
tablicy name więcej niŜ 20 znaków, uŜyto manipulatora setw(20) zadeklarowanego w nagłówku iomanip.h.
Zmienne strukturowe
Zmienną strukturową, w skrócie strukturą, jest zestaw sąsiadujących ze sobą elementów struktury. KaŜdy
element struktury moŜe być zmienną innego typu: skalarną, tablicową, strukturową.
Przed zadeklarowaniem zmiennej strukturowej naleŜy zdefiniować jej typ. Deklaracja typu strukturowego składa
się z deklaracji pól struktury. Deklaracja pola struktury ma postać deklaracji zmiennej.
struct Child {
char name[20];
int age;
};
Child isa = { "Isabel", 15 };
Struktura isa składa się z 2 zmiennych, opisanych przez pola name i age. Wartości początkowe elementów
struktury określono za pomocą inicjatora klamrowego. UŜycie innych inicjatorów jest zabronione.
Identyfikowanie elementów
Jeśli nazwą struktury jest str, a w opisie jej typu występuje pole fld, to nazwą zmiennej odpowiadającej temu
polu jest str.fld.
#include <iostream.h>
#include <iomanip.h>
struct Child {
char name[20];
int age;
};
Child child;
int main(void)
{
cin >> setw(20) >> child.name >> child.age;
cout << child.name << " is "
<< child.age << " now" << endl;
return 0;
}
Zmienna child składa się z tablicy o elementach typu char i zmiennej skalarnej typu int. Program wprowadza
imię i wiek dziecka, a następnie wyprowadza je.
Kopiowanie struktur
W odróŜnieniu od tablic, które moŜna kopiować tylko element-po-elemencie, kopiowanie struktur moŜe dotyczyć
pełnego zestawu jej elementów i to nawet wówczas, gdy struktura zawiera tablice.
#include <iostream.h>
struct Child {
char name[20];
int age;
};
Child girl;
17
int main(void)
{
Child isa = { "Isabel", 15 };
girl = isa;
cout << girl.name << " is " << girl.age << endl;
return 0;
}
Program wyprowadza te same dane, którymi zainicjowano strukturę isa.
Unia elementów
Struktura, której elementy są rozmieszczone w pamięci nie jeden-za-drugim, ale zawsze od tego samego miejsca,
jest nazywana unią. W celu zadeklarowania unii naleŜy zamiast słowa kluczowego struct uŜyć słowa union.
Definicja unii, w której pominięto nazwę typu, jest definicją unii anonimowej. Pola unii anonimowej są
zadeklarowane w miejscu zdefiniowania unii.
struct Number {
bool isFixed;
union { // unia anonimowa
int fixed;
double real;
};
};
Number num = { true, 12 };
if(num.isFixed)
cout << num.fixed << endl; // 12
else
cout << num.real << endl;
cout << num.real << endl; // bł
ą
d
W kaŜdej chwili struktura num składa się ze zmiennych typu bool i int, albo ze zmiennych typu bool i double.
Błąd polega na tym, Ŝe w chwili gdy zmienna num składa się ze zmiennych typu bool i int, następuje odwołanie
do zmiennej typu double.
Przetwarzanie plików
Przetwarzanie plików odbywa się za pośrednictwem zmiennych strumieniowych klas ifstream i ofstream,
zadeklarowanych w nagłówku fstream.h. Po utworzeniu zmiennej strumieniowej naleŜy otworzyć skojarzony z
nią plik, a następnie upewnić się, Ŝe otwarcie było pomyślne.
Po pomyślnym otwarciu pliku, pochodzący z niego strumień danych moŜna przetwarzać w taki sam sposób, jak
strumień danych związany z klawiaturą albo z monitorem.
Stany strumienia
Początkowo strumień znajduje się w stanie dobrym, ale na skutek błędu operacji wejścia-wyjścia albo próby
wprowadzenia nieistniejącej danej, moŜe znaleźć się w stanie nie-dobrym (fail).
W stanie nie-dobrym wszystkie operacje na strumieniu są ignorowane. Jeśli dane przygotowano właściwie, a
jakość pamięci zewnętrznej jest zadowalająca, to stan nie-dobry oznacza, Ŝe napotkano koniec strumienia.
18
Uwaga: W programach przykładowych nie będzie rozpatrywany przypadek wystąpienia błędu przesyłania
danych.
Szczególnym przypadkiem stanu nie-dobrego jest stan zły (bad). Powstaje on w przypadku rozpoznania danych o
złym formacie. Niestety, na skutek niefortunnych domniemań, wprowadzenie takiej "danej" jak 3e, zamiast 3e0
(w kontekście 3ex) nie zmienia stanu strumienia na zły.
Uwaga: Do sprawdzenia czy stan strumienia jest zły, słuŜy funkcja bad, a do sprawdzenia, czy strumień znajduje
się w pozycji za końcem pliku, słuŜy funkcja eof. Funkcji tych uŜywa się bardzo rzadko.
Zmienna plikowa
Jeśli w miejscu wystąpienia operacji wejścia-wyjścia odbywa się takie badanie zmiennej plikowej, jakby
dotyczyło wyraŜenia o wartości orzecznikowej, na przykład
while(cin >> num) ...
albo
if(cin) ...
to w stanie dobrym jest dostarczana wartość true, a w stanie nie-dobrym wartość false.
Wprowadzanie danych
Zmienna strumieniowa uŜyta do wprowadzania danych z pliku jest typu ifstream. Otwarcie pliku odbywa się za
pomocą funkcji open, której pierwszym argumentem jest nazwa, a drugim tryb otwarcia pliku: ios::in. Jeśli
otwierany plik nie istnieje, to zostanie utworzony jako pusty. Aby tego uniknąć, plik naleŜy otworzyć w trybie
ios::in | ios::nocreate.
Do zbadania, czy otwarcie pliku się powiodło, słuŜy funkcja is_open. Jej rezultat ma wartość nie-zero tylko
wówczas, gdy otwarcie było pomyślne.
#include <iostream.h>
#include <fstream.h>
#include <assert.h>
int sum = 0;
int main(void)
{
ifstream inp; // zmienna plikowa
inp.open("Data.txt", ios::in | ios::nocreate);
if(!inp.is_open()) {
cout << "File does not exist" << endl;
return -1;
}
int val;
while(inp >> val) // wprowad
ź
i sprawd
ź
stan
sum += val; // dosumuj
assert(!inp.bad()); // raczej zb
ę
dne
cout << "Sum = " << sum << endl;
return 0;
}
Wykonanie programu powoduje wyprowadzenie sumy liczb całkowitych zawartych w pliku Data.txt.
Wywołanie funkcji assert ma na celu upewnienie się, Ŝe strumień nie znajduje się w złym stanie. Gdyby tak było,
to wykonanie programu zostałoby zaniechane.
19
Wyprowadzanie danych
Zmienna strumieniowa uŜyta do wyprowadzania danych do pliku jest typu ofstream. Otwarcie pliku odbywa się
za pomocą funkcji open, której pierwszym argumentem jest nazwa, a drugim tryb otwarcia pliku: ios::out.
Jeśli otwierany plik nie istnieje, to zostanie utworzony i otworzony jako pusty. Jeśli juŜ istnieje, to zostanie
otworzony jako pusty.
Do zbadania, czy otwarcie pliku się powiodło, słuŜy funkcja is_open. Jej rezultat ma wartość nie-zero tylko
wówczas, gdy otwarcie było pomyślne.
#include <iostream.h>
#include <fstream.h>
int main(void)
{
ifstream inp;
inp.open("Data.txt", ios::in | ios::nocreate);
if(!inp.is_open()) {
cout << "Source does not exist" << endl;
return -1;
}
ofstream out;
out.open("Data2.txt", ios::out);
if(!out.is_open()) {
cout << "Target not opened" << endl;
return -1;
}
int val;
while(inp >> val)
out << val << endl;
cout << "Done!" << endl;
return 0;
}
Program kopiuje liczby całkowite z pliku Data.txt do pliku Data2.txt. KaŜdą kopiowaną liczbę umieszcza w
nowym wierszu.
UŜycie klawiatury
Jeśli dane wprowadza się z klawiatury, to koniec strumienia określa się za pomocą znaku końca: Ctrl-Z (na
polskiej klawiaturze Ctrl-Y). W Visual C++ nastąpi wówczas pominięcie pierwszego znaku wyprowadzonego na
konsolę.
Uwaga: Zaleca się, aby znak końca został wprowadzony na początku nowego wiersza (po Enter).
#include <iostream.h>
int main(void)
{
int count = 0;
double tmp;
while(cin >> tmp)
count++;
cout << endl; // na po
Ŝ
arcie
cout << "Count = " << count << endl;
return 0;
}
20
Program zlicza dane liczbowe wprowadzone z klawiatury.
21
Ś
rodowisko Visual C++
Program źródłowy składa się z modułów źródłowych. KaŜdy moduł jest umieszczony w odrębnym pliku z
rozszerzeniem .cpp. Dodatkowo, w skład programu mogą wchodzić moduły skompilowane (*.obj) i biblioteczne
(*.lib).
W celu przekształcenia zestawu modułów w program wykonalny, naleŜy utworzyć projekt, umieścić go w
przestrzeni roboczej, włączyć do projektu nazwy plików z rozszerzeniami .cpp, .obj i .lib, a następnie zbudować
program. Zostanie on umieszczony w pliku z rozszerzeniem .exe.
Katalog
Zaleca się, aby pliki programu znajdowały się we własnym katalogu. Jeśli dysponuje się wolnym miejscem na
przykład w katalogu głównym dysku D:, to naleŜy wywołać Eksplorator Windows, kliknąć na nazwie katalogu
głównego i wydać polecenie Plik / Nowy obiekt / Folder, a następnie określić nazwę swojego katalogu, na
przykład jbVisual.
Przestrzeń
W celu utworzenia przestrzeni roboczej naleŜy wydać polecenie File / New, a następnie (w zakładce
Workspaces) podać nazwę przestrzeni, np. Workspace: jbSpace oraz określić jej połoŜenie, np. Location:
D:\jbVisual\jbSpace, po czym nacisnąć przycisk OK.
Jeśli przestrzeń juŜ istnieje, to aby ją otworzyć, naleŜy wydać polecenie File / Open Workspace, wejść do
katalogu przestrzeni (np. jbSpace), a następnie dwu-kliknąć na nazwie jbSpace.dsw.
Projekt
W celu utworzenia projektu naleŜy wydać polecenie File / New, a w zakładce Projects podać typ projektu: Win
32 Console Application i jego nazwę, np. Project name: jbTests. Po upewnieniu się, Ŝe projekt zostanie
włączony
do
bieŜącej
przestrzeni
(Add
to
current
workspace)
o
czym
zaświadczy
Location: D:\jbVisual\jbSpace\jbTests, naleŜy nacisnąć przycisk OK.
Pliki
W celu utworzenia plików projektu naleŜy wydać polecenie File / New, a następnie (w zakładce Files), określić
rodzaj pliku
C/C++ Source File
dla pliku z rozszerzeniem .cpp
C++ Header File
dla pliku z rozszerzeniem .h
Text File
dla pliku z rozszerzeniem .txt
nie zapominając o podaniu jego nazwy (bez rozszerzenia), np. File name: Sum.
Po wykonaniu tych czynności, w katalogu D:\jbVisual\jbSpace\jbTests powstanie plik Sum.cpp, a jego
(początkowo pusta) zawartość ujawni się odrębnym oknie edycyjnym.
22
Jeśli program wymaga utworzenia plików z danymi, to zaleca się je umieścić w tym samym katalogu co pliki
ź
ródłowe. Dla wygody moŜna je dołączyć do plików projektu.
Budowanie projektu
W celu zbudowania projektu, to jest skompilowania jego wszystkich plików *.cpp, oraz ewentualnie jego plików
*.obj i *.lib, naleŜy kliknąć ikonę Build. Spowoduje to niezaleŜne kompilacje wszystkich modułów źródłowych
oraz połączenie ich w program wykonalny.
Przebieg budowania projektu jest diagnozowany w oknie Output. Jeśli okno nie jest widoczne, to moŜna je
wyświetlić wydając polecenie View / Output.
Błędy modułu wyszczególnia się w oknie Output. Po rozpoznaniu kaŜdego z nich podaje się krótki opis
przyczyny błędu i numer wiersza programu. Dwu-kliknięcie w obrębie opisu błędu powoduje przeniesienie
kursora w pobliŜe miejsca, w którym wykryto błąd.
W rzadkich przypadkach, gdy poprawność diagnozy budzi wątpliwości, zaleca się zastąpienie polecenia Build
poleceniem Build / Rebuild All.
Wykonanie programu
Program wykonalny, pod nazwą jbTests.exe jest umieszczany w podkatalogu jbTests\Debug. Jeśli wykonuje się
bezbłędnie i jest naleŜycie wytestowany, to moŜe zostać zoptymalizowany.
W celu zoptymalizowania programu naleŜy wydać polecenie Build / Set Active Configuration, a następnie
zamiast konfiguracji Win 32 Debug, wybrać konfigurację Win 32 Release. Po ponownym zbudowaniu projektu,
w katalogu jbTests\Release, powstanie program znacznie krótszy, ale juŜ bez informacji uruchomieniowych.
Zarządzanie projektami
Przestrzeń robocza moŜe zawierać więcej niŜ jeden projekt, a projekt moŜe składać się z więcej niŜ jednego
pliku.
Jeśli przestrzeń zawiera więcej niŜ jeden projekt, to tylko jeden z nich moŜe być aktywny, to jest taki, którego
dotyczą polecenia Build. Uaktywnienie projektu odbywa się przez p-kliknięcie jego nazwy i wydanie polecenia
Set Active Project.
W celu umieszczenia w przestrzeni dodatkowego projektu naleŜy p-kliknąć nazwę przestrzeni, wydać polecenie
Add New Project to Workspace, a dalej postępować tak, jak podczas tworzenia pierwszego projektu.
W celu włączenia do projektu dodatkowego pliku naleŜy p-kliknąć nazwę projektu, a następnie wydać polecenie
Add Files to Project i wybrać skopiowany plik.
Jeśli włączany do projektu plik źródłowy juŜ istnieje, to naleŜy skopiować go do katalogu projektowego
(posługując się np. Eksploratorem Windows), a następnie postąpić tak, jak podczas dodawania pliku do
projektu.
Dopasowanie oblicza
Oblicze środowiska uruchomieniowego składa się z menu oraz z pasków, które moŜna konfigurować. Odbywa
się to za pomocą polecenia Tools / Customize umoŜliwiającego zarządzanie wyświetlaniem pasków edycyjnych,
uruchomieniowych i innych.
23
Uruchamianie programu
Systematyczne wyszukiwanie błędów w programie odbywa się za pomocą uruchamiacza. W celu wyświetlenia
paska zawierającego jego narzędzia naleŜy wydać polecenie Tools / Customize / Toolbars, a następnie odhaczyć
nastawę Debug.
Wykonanie programu nadzorowanego przez uruchamiacz zaczyna się w konfiguracji Win32 Debug po wydaniu
polecenia Build / Start Debug / Step into (F10). Program zatrzyma się tuŜ przed przystąpieniem do wykonania
pierwszej funkcji (zazwyczaj funkcji main).
Począwszy od tego momentu moŜna
Określać argumenty funkcji głównej
Project / Settings // Debug, Program arguments
Zastawiać / usuwać pułapki
ikona Hand (F9)
Usuwać pułapki
Edit / Breakpoints / Remove All (Alt-F9)
Wykonywać program krokowo
ikona Go (po zastawieniu pułapki)
ikona Step over (F10)
ikona Step into (F11)
ikona Step out (Shift-F11)
Obserwować zmienne
ikona Quick Watch (Shift-F9)
Kompilacja warunkowa
Podczas uruchamiania programu przydaje się ignorowanie jego wybranych fragmentów. Odbywa się to za
pomocą dyrektyw kompilacji warunkowej: #if, #else, #endif.
Zinterpretowanie dyrektywy
#if c
kod-źródłowy
#else
kod-alternatywny
#endif
zaczyna się od wyznaczenia wartości wyraŜenia c (najczęściej liczby 1 albo 0). Jeśli wyraŜenie ma wartość róŜną
od 0, to całą dyrektywę zastępuje się napisem kod-źródłowy. W przeciwnym razie zastępuje się ją napisem kod-
alternatywny.
Uwaga: Jeśli napis kod-źródłowy jest pusty, to dyrektywę moŜna zapisać bez frazy #else.
#include <iostream.h>
int main(void)
{
int one, two;
cin >> one >> two;
#if 1
24
cout << "Sum = ";
#endif
cout << one + two << endl;
return 0;
}
Program wyprowadza sumę pary danych wejściowych poprzedzając ją napisem Sum =. Jeśli w dyrektywie #if
zmieni się 1, na 0, to powstanie program, który takiego napisu nie wyprowadzi.
25
Wskaźniki i odnośniki
Wskaźniki i odnośniki są zmiennymi, które słuŜą do identyfikowania innych zmiennych. Wskaźnik moŜe
identyfikować wiele zmiennych pokrewnego mu typu, natomiast odnośnik moŜe identyfikować tylko jedną
zmienną.
Wskaźnikom przypisuje się wskazania, a odnośnikom odniesienia. Mimo iŜ w typowych implementacjach
zarówno wskazania jak i odniesienia są reprezentowane przez adresy, posługiwanie się pojęciem adres jest
całkowicie zbyteczne i dowodzi myślenia o C++ nie w kategoriach języka wysokiego poziomu, ale w
kategoriach implementacji. Dlatego o adresach nie będzie juŜ mowy.
Zmienne wskaźnikowe
Wskaźnikiem jest zmienna, której moŜna przypisywać wskazania. Deklarację wskaźnika moŜna poznać po tym,
Ŝ
e jej identyfikator jest poprzedzony symbolem * (gwiazdka).
Jeśli w pewnym miejscu programu jest wymagane uŜycie wskazania zmiennej, to otrzymuje się je poprzedzając
nazwę zmiennej operatorem wskazywania & (ampersand).
Po przypisaniu wskaźnikowi ptr wskazania zmiennej, napis *ptr staje się chwilową nazwą tej zmiennej. Po
przypisaniu wskaźnikowi wskazania pustego (reprezentowanego przez liczbę 0), uŜycie nazwy *ptr albo nazwy
jej równowaŜnej (np. ptr[0]) jest zabronione.
int fix1 = 10,
fix2 = 20;
int *ptr = &fix1;
cout << *ptr; // 10
*ptr = 11;
cout << *ptr << fix; // 11 11
ptr = &fix2;
cout << *ptr; // 20
*ptr = 22;
cout << *ptr << fix; // 22 22
ptr = 0;
cout << *ptr; // bł
ą
d
Wskaźnik ptr jest przystosowany do wskazywania zmiennych typu int. Przypisano mu kolejno: wskazanie
zmiennej fix1, wskazanie zmiennej fix2 i wskazanie puste.
Po przypisaniu wskaźnikowi ptr wskazania zmiennej fix1, napis *ptr jest chwilową nazwą zmiennej fix1, a po
przypisaniu mu wskazania zmiennej fix2, jest chwilową nazwą zmiennej fix2.
Po przypisaniu wskaźnikowi ptr wskazania pustego, aŜ do chwili przypisania mu wskazania zmiennej, uŜycie
nazwy *ptr jest zabronione.
Dla dociekliwych
Typ wyraŜenia inicjującego wskaźnik musi być zgodny z typem wskaźnika. Przyjmuje się z definicji, Ŝe zgodne
ze wskaźnikiem typu Type jest kaŜde wyraŜenie typu Type oraz kaŜde wyraŜenie, które moŜe być poddane
niejawnej konwersji do typu Type (por. Dodatek C).
26
char *ptr1 = "0\0\0\0" // niejawna konwersja
int *ptr2 = "0\0\0\0"; // bł
ą
d
int *ptr = (int *)"0\0\0\0"; // jawna konwersja
cout << *ptr; // 48 (kod cyfry 0)
Skutek uŜytej tu jawnej konwersji zaleŜy od implementacji. W Visual C++ powoduje to potraktowanie obszaru
pamięci zajętego przez pierwsze 4 bajty literału jako zmiennej całkowitej.
Wskaźniki i tablice
Związki między wskaźnikami i tablicami są bardzo bliskie. KaŜda nazwa tablicy jest niejawnie przekształcana na
wskaźnik jej zerowego elementu, a kaŜda nazwa wskaźnika moŜe być indeksowana tak, jak nazwa tablicy.
Jeśli wskaźnik ptr wskazuje pewien element tablicy, to zarówno *ptr jak i ptr[0] jest nazwą tego elementu.
Elementy połoŜone z lewej strony elementu wskazywanego mają nazwy ptr[-1], ptr[-2], itd., a elementy połoŜone
z prawej mają nazwy ptr[1], ptr[2], itd.
Jeśli i jest wyraŜeniem całkowitym, to wyraŜenie ptr+i jest wskaźnikiem elementu odległego o i elementów od
wskazywanego (dla i ujemnego - w lewo, a dla i dodatniego - w prawo).
Jeśli wskaźniki ptr1 i ptr2 wskazują odpowiednio elementy o indeksach i oraz j tej samej n-elementowej tablicy
(a takŜe gdy wskazują nie istniejące "elementy" o indeksach -1 i n), to wyraŜenie ptr1-ptr2 ma wartość i-j.
int vec[3] = { 10, 20, 30 };
int *ptr = vec + 2;
cout << ptr++[-1]; // 20
cout << *(vec + 2); // 30
cout << vec - ptr; // -3
Nazwa vec zostaje niejawnie przekształcona na wskazanie elementu vec[0], to jest na &vec[0].
WyraŜenie vec + 2 wskazuje element o wartości 30.
WyraŜenie ptr++[-1] jest nazwą elementu o wartości 20.
W wyraŜeniu vec - ptr pierwszy argument wskazuje element zerowy, a drugi argument wskazuje nie istniejący
element vec[3].
Wskaźniki i struktury
Jeśli wskaźnik ptr wskazuje strukturę o polu f, to nazwą zmiennej odpowiadającej temu polu jest (*ptr).f, albo
krócej ptr->f.
#include <iostream.h>
struct Child {
char name[20];
int age;
Child *pNext;
};
Child bob = { "Robert", 20 },
tom = { "Thomas", 30, 0 };
Child *pBob = &bob,
*pTom = &tom;
int main(void)
{
cout << pBob->name << endl; // Robert
27
pBob->pNext = pTom;
cout << pBob->pNext->age << endl; // 30
return 0;
}
Zmienna pBob jest wskaźnikiem przystosowanym do wskazywania zmiennych typu Child. Przypisano jej
wskazanie struktury bob.
Napis pBob->name jest chwilową nazwą tego elementu struktury bob, który jest opisany przez pole name.
Napis pBob->pNext jest nazwą wskaźnika opisanego przez pole pNext. PoniewaŜ wskazuje on strukturę tom,
więc pBob->pNext->age jest nazwą tego elementu struktury tom, który jest opisany przez pole age.
Tablice wskaźników
Tablicą wskaźników jest tablica, której elementami są wskaźniki. W deklaracji tablicy wskaźników jej
identyfikator jest poprzedzony znakiem * (gwiazdka).
W deklaracji wskaźnika, który słuŜy do wskazywania-wskaźników, jego identyfikator jest poprzedzony dwiema
znakami * (gwiazdka).
#include <iostream.h>
const int Count = 3;
struct Child {
char name[20];
int age;
};
Child john = { "John Smith", 30 },
tom = { "Thomas Mill", 10 },
bill = { "Robert Dole", 20 };
Child *pBoys[Count] = { &john, &tom, &bill };
int main(void)
{
for(int i = 0; i < Count-1 ; i++) {
int minAge = pBoys[i]->age;
for(int j = i+1; j < Count ; j++) {
if(pBoys[j]->age < minAge) {
minAge = pBoys[j]->age;
Child *ptr = pBoys[i];
pBoys[i] = pBoys[j];
pBoys[j] = ptr;
}
}
}
Child **ptr = pBoys;
for(i = 0; i < Count ; i++)
cout << (*ptr++)->name << endl;
return 0;
}
Program wyprowadza nazwiska chłopców, w kolejności ich rosnącego wieku. Sortowanie dotyczy tylko
elementów tablicy wskaźników i nie powoduje kopiowania struktur typu Child.
Wskaźniki a ustalenia
28
Podobnie jak zwykła zmienna, tak i wskaźnik moŜe być ustalony albo nie-ustalony. Ponadto wskaźnik moŜe być
przystosowany do wskazywania zmiennych ustalonych albo nie-ustalonych. Daje to cztery moŜliwości.
Uwaga: Zabrania się, aby wskaźnikowi przystosowanemu do wskazywania zmiennych nie-ustalonych przypisano
wskazanie zmiennej ustalonej.
#include <iostream.h>
int main(void)
{
int mod = 10;
const int fix = 20;
int *ptr1 = &mod;
int *const ptr2 = &mod;
const int *ptr3 = &mod;
const int *const ptr4 = &fix;
cout << *ptr1 << endl; // 10
cout << *ptr2 << endl; // 10
cout << *ptr3 << endl; // 10
cout << *ptr4 << endl; // 20
ptr1 = &fix; // bł
ą
d
++ptr2; // bł
ą
d
++*ptr3; // bł
ą
d
ptr1 = &(int &)fix; // dozwolone
cout << *ptr1 << endl; // 20
return 0;
}
Wskaźnik ptr1 słuŜy do wskazywania zmiennych nie-ustalonych. Wskaźnik ptr2 jest wskaźnikiem ustalonym, który
słuŜy do wskazywania zmiennych nie-ustalonych. Wskaźnik ptr3 jest wskaźnikiem nie-ustalonym, który słuŜy do
wskazywania zmiennych ustalonych. Wskaźnik ptr4 jest wskaźnikiem ustalonym, który słuŜy do wskazywania
zmiennych ustalonych.
Zmienne odnośnikowe
Odnośnikiem jest zmienna, którą moŜna zainicjować odniesieniem. Deklarację odnośnika moŜna poznać po tym,
Ŝ
e jej identyfikator jest poprzedzony symbolem & (ampersand). Istnieją odnośniki do zmiennych, ale nie istnieją
tablice odnośników. KaŜdy odnośnik musi być zainicjowany.
Uwaga: Jeśli w pewnym miejscu programu występuje nazwa zmiennej, a program byłby poprawny tylko
wówczas, gdyby występowała tam nazwa odnośnika do zmiennej, to nazwę zmiennej niejawnie przekształca się
w odnośnik.
int fix = 10;
int &ref = fix; // int &ref = (int &)fix;
PoniewaŜ odnośnik ref jest typu int &, więc nie moŜe być zainicjowany wartością zmiennej fix, która jest typu
int. Dlatego, za pomocą niejawnej konwersji (int &)fix, nazwę zmiennej fix niejawnie przekształca się w
odnośnik.
Po zainicjowaniu odnośnika ref odniesieniem do zmiennej, napis ref staje się trwałą nazwą tej zmiennej. A więc
odnośnik moŜna zainicjować, ale nie moŜna mu przypisać odniesienia.
#include <iostream.h>
29
int main(void)
{
int fix = 10;
int &ref = fix;
ref = 10;
cout << fix << ref << endl; // 10 10
return 0;
}
Po zainicjowaniu odnośnika, napis ref staje się trwałą nazwą zmiennej fix. Dlatego przypisanie ref = 10 zmienia
wartość zmiennej fix, ale nie zmienia wartości odnośnika ref.
Dla dociekliwych
Typ wyraŜenia inicjującego odnośnik musi być zgodny z typem odnośnika. Przyjmuje się z definicji, Ŝe typ
"odnośnik do zmiennej typu Type" (np. int &) jest zgodny z typem Type (np. int). Jeśli wyraŜenie inicjujące jest
innego typu, to moŜe być poddane niejawnej konwersji do typu zgodnego, ale tylko wówczas, gdy typ odnośnika
jest ustalony (const). W takim wypadku odnośnik zostanie zainicjowany odniesieniem do zmiennej pomocniczej
typu z nim zgodnego, zainicjowanej wartością wyraŜenia po konwersji.
int &ref1 = 2.4; // bł
ą
d
const int &ref = 2.4;
Identyfikator ref2 jest trwałą nazwą zmiennej pomocniczej o wartości (int)2.4.
Wskaźniki i odnośniki
Podejmując decyzję o uŜyciu wskaźnika, czy odnośnika, naleŜy kierować się wytyczną, Ŝe wszędzie tam gdzie
jest to moŜliwe, naleŜy stosować odnośniki, gdyŜ zwiększa to czytelność programu.
W rzadkich przypadkach stosuje się odnośniki do wskaźników. Jest to niezbędne wówczas, gdy poprzez
odnośnik naleŜy zmodyfikować wskaźnik.
#include <iostream.h>
int main(void)
{
int vec[3] = { 10, 20, 30 };
int *ptr = vec;
int *&ref = ptr;
++ref;
cout << *ptr << endl; // 20
return 0;
}
Po zadeklarowaniu odnośnika, napis ref jest trwałą nazwą wskaźnika ptr. Dlatego po wykonaniu operacji ++ref
wskaźnik ptr wskazuje element vec[1] o wartości 20.
Gdyby z deklaracji odnośnika usunięto znak &, to napis ref stałby się nazwą wskaźnika zainicjowanego
wskazaniem elementu vec[0], a wykonanie operacji ++ref nie miałoby Ŝadnego wpływu na wskaźnik ptr. W takim
wypadku nastąpiłoby wyprowadzenie liczby 10.
30
Przetwarzanie łańcuchów
Łańcuchem jest dowolna sekwencja elementów tablicy znakowej, zakończona elementem o wartości 0. PoniewaŜ
kaŜdy literał łańcuchowy jest nazwą takiej właśnie sekwencji elementów, więc jest nazwą łańcucha.
W szczególności, literał "Hello" jest nazwą 6-elementowej tablicy znakowej, której element "Hello"[0] ma
wartość 'H', a element "Hello"[5] ma wartość 0.
Do typowych operacji wykonywanych na łańcuchach naleŜą: wprowadzenie i wyprowadzenie łańcucha,
wyznaczenie długości łańcucha (strlen), skopiowanie łańcucha (strcpy), połączenie łańcuchów (strcat) i
porównanie łańcuchów (strcmp). Operacje te moŜna wykonać za pomocą funkcji bibliotecznych,
zadeklarowanych w nagłówku string.h.
Uwaga: Jeśli wskaźnik wskazuje pierwszy element łańcucha, to mówi się w skrócie, Ŝe wskazuje łańcuch..
int strlen(char *pStr)
Dostarcza liczbę znaków łańcucha wskazanego przez argument.
np.
cout << strlen("Hello"); // 5
char *strcpy(char *pTrg, const char *pSrc)
Dostarcza pierwszy argument. Ponadto kopiuje, począwszy od miejsca wskazanego przez pierwszy argument,
łańcuch wskazany przez drugi argument.
np.
char buf[100] = "Hello ";
cout << strcpy(buf + 6, "World")- 6; // Hello World
char *strcat(char *pTrg, const char *pSrc)
Dostarcza pierwszy argument. Ponadto kopiuje, poczawszy od miejsca, w którym znajduje się znak końca
łańcucha wskazanego przez pierwszy argument, łańcuch wskazany przez drugi argument.
np.
char buf[100] = "Hello ";
cout << strcat(buf, "World"); // Hello World
int strcmp(const char *pOne, const char *pTwo)
Dostarcza wartość +1 jeśli łańcuch wskazany przez pierwszy argument jest większy niŜ łańcuch wskazany przez
drugi argument, dostarcza wartość 0 jeśli są równe, albo wartość -1 jeśli pierwszy jest mniejszy.
Uwaga: Porównanie łańcuchów zastępuje się porównaniem pierwszej pary znaków róŜnych. Jeśli jeden z
łańcuchów jest podłańcuchem drugiego, to za większy uznaje się dłuŜszy.
np.
cout << strcmp("abc", "abaaaaa"); // -1
cout << strcmp("abcde", "ab"); // 1
cout << strcmp("ab", "ab"); // 0
Wprowadzanie i wyprowadzanie łańcuchów
Operacja wprowadzenia łańcucha ma postać cin >> ptr, w której ptr jest wskaźnikiem elementu tablicy
znakowej. Jej wykonanie powoduje umieszczenie w tablicy, począwszy od jej elementu *ptr, kodów spójnego
ciągu znaków wejściowych oraz kodu znaku '\0' (o wartości 0).
Przed wprowadzeniem znaków zostaną pominięte odstępy wiodące. W celu zabezpieczenia się przed
przepełnieniem tablicy moŜna uŜyć manipulatora setw.
31
Operacja wyprowadzenia łańcucha ma postać cout << ptr, w której ptr jest wskaźnikiem. Zabrania się, aby ptr
było wskaźnikiem elementu tablicy, który nie jest zerowym elementem łańcucha.
#include <iostream.h>
#include <string.h>
const int Size = 100;
char buffer[Size] = "prof. ";
int main(void)
{
int len1 = strlen(buffer);
cin >> buffer + len1;
int len2 = strlen(buffer);
buffer[len2] = ' ';
buffer[len2+1] = 0;
cin >> buffer + len2 + 1;
cout << buffer << endl;
cout << "dr " << buffer + len1 << endl;
return 0;
}
Jeśli z klawiatury wprowadzi się imię i nazwisko (np. Jan Bielecki), to program wyprowadzi to imię i to nazwisko
poprzedzone napisem prof. (np. prof. Jan Bielecki), a ponadto tylko to imię i to nazwisko.
Wyznaczenie długości
#include <iostream.h>
#include <string.h>
char str[6] = "Hello";
int main(void)
{
cout << strlen("Hello") << endl; // 5
char *ptr = str;
int len = 0;
while(*ptr != 0) {
len++;
ptr++;
}
cout << len << endl; // 5
ptr = str;
len = 0;
while(*ptr++)
len++;
cout << len << endl; // 5
return 0;
}
Pokazano trzy sposoby wyznaczenia długości łańcucha zapisanego w tablicy znakowej.
WyraŜenie *ptr++ jest nazwą zmiennej, wskazywanej przez wskaźnik ptr, przed wykonaniem na nim operacji
zwiększenia.
Kopiowanie
#include <iostream.h>
#include <string.h>
char src[7] = "Hello ";
32
char trg[100];
int main(void)
{
char *pSrc = src,
*pTrg = trg;
strcpy(pTrg, pSrc);
cout << trg << endl; // Hello
while(*pSrc != 0) {
*pTrg = *pSrc;
pSrc++;
pTrg++;
}
pTrg = 0;
cout << trg << endl; // Hello
pSrc = src;
pTrg = trg;
while(*pTrg++ = *pSrc++)
;
cout << trg << endl; // Hello
return 0;
}
Pokazano trzy sposoby kopiowania łańcucha znaków.
Łączenie
#include <iostream.h>
#include <string.h>
char *pSrc = "Hello",
buf[100];
int main(void)
{
strcat(strcpy(buf, pSrc), "!");
cout << buf << endl; // Hello!
char *pBuf = buf;
strcpy(pBuf, pSrc);
while(*pBuf++)
;
char *pSrc = "!";
while(pBuf++[-1] = *pSrc++)
;
cout << buf << endl; // Hello!
return 0;
}
Pokazano dwa sposoby łączenia łańcuchów.
Porównanie
#include <iostream.h>
#include <string.h>
char one[100],
two[100];
int main(void)
{
cin >> one >> two;
33
cout << one;
switch(strcmp(one, two)) {
case +1:
cout << " > ";
break;
case -1:
cout << " < ";
break;
default:
cout << " == ";
}
cout << two << endl;
char *pOne = one,
*pTwo = two;
cout << one;
while(*pOne == *pTwo && *pOne != 0) {
pOne++;
pTwo++;
}
if(*pOne == 0 && *pTwo == 0)
cout << " == ";
else if(*pOne > *pTwo)
cout << " > ";
else
cout << " < ";
cout << two << endl;
pOne = one;
pTwo = two;
cout << one;
while(*pOne || *pTwo) {
if(*pOne++ != *pTwo++) {
if(pOne[-1] > pTwo[-1])
cout << " > ";
else
cout << " < ";
cout << two << endl;
return 0;
}
}
cout << " == ";
cout << two << endl;
return 0;
}
Pokazano trzy sposoby porównywania łańcuchów wprowadzonych z klawiatury.
34
Posługiwanie się funkcjami
Funkcja jest sparametryzowanym opisem czynności. W miejscu wywołania funkcji musi być znana jej
deklaracja albo definicja. W szczególności oznacza to, Ŝe wywołanie
sum(10, 20)
funkcji sumującej argumenty, musi być poprzedzone
albo jej definicją
int sum(int one, int two)
{
return one + two;
}
albo jej deklaracją
int sum(int one, int two);
albo włączeniem nagłówka zawierającego deklarację.
Uwaga: W deklaracji funkcji moŜna pominąć dowolny zestaw identyfikatorów parametrów. Jeśli uczyni się to w
definicji, to uniemoŜliwi to odwoływanie się do argumentów.
Parametry i argumenty
Wywołanie funkcji zaczyna się od skojarzenia jej parametrów z argumentami. Skojarzenie parametru z
argumentem odbywa się przez-wartość, co oznacza, Ŝe parametr jest traktowany tak, jak lokalna zmienna funkcji,
zadeklarowana tuŜ przed jej pierwszą instrukcją i zainicjowana wartością argumentu.
A zatem, jeśli definicją funkcji jest
int sum(int one, int two)
{
return one + two;
}
to dla wywołania
sum(10, 20)
funkcja jest traktowana tak, jakby miała postać
int sum()
{
int one = 10;
int two = 20;
return one + two;
}
35
Parametry zwykłe
Parametr funkcji jest "zwykły", jeśli nie jest wskaźnikiem ani odnośnikiem. Z parametrem zwykłym moŜna
skojarzyć argument, który jest takiego samego typu jak parametr, albo który moŜna poddać niejawnej konwersji
do typu parametru.
Zainicjowanie parametru polega na skopiowaniu argumentu. Jeśli argument jest strukturą, to kopiuje się
wszystkie jej elementy (co w przypadku duŜych struktur ma oczywiste wady!).
Po dokonaniu skojarzenia, wszelkie operacje wykonywane na parametrze dotyczą lokalnej zmiennej
zainicjowanej argumentem i nie powodują zmiany wartości skojarzonego z nim argumentu.
#include <iostream.h>
int main(void)
{
void inc(int par);
int fix = 10;
cout << fix << endl; // 10
inc(fix);
cout << fix << endl; // 10
return 0;
}
void inc(int par)
{
++par;
}
Program potwierdza, Ŝe wykonanie operacji na parametrze "zwykłym" nie powoduje zmiany wartości
skojarzonego z nim argumentu.
Parametry wskaźnikowe
Z parametrem wskaźnikowym moŜna skojarzyć argument, który jest takiego samego typu jak parametr, albo
który moŜna poddać niejawnej konwersji do typu parametru.
Zainicjowanie parametru polega na skopiowania wskaźnika. Nie pociąga to za sobą kopiowania zmiennej
identyfikowanej przez argument (co moŜna wykorzystać w przypadku duŜych struktur!).
Po dokonaniu skojarzenia, wszelkie operacje wykonywane na parametrze dotyczą lokalnej zmiennej
zainicjowanej argumentem, ale operacje wykonywane za pośrednictwem parametru (np. *par, par[i] albo par->f)
dotyczą zmiennej wskazywanej przez argument. MoŜe to mieć wpływ na wartość argumentu.
#include <iostream.h>
int main(void)
{
void inc(int *ptr);
int fix = 10;
cout << fix << endl; // 10
inc(&fix);
cout << fix << endl; // 11
return 0;
}
void inc(int *ptr)
{
++*ptr;
}
36
Program potwierdza, Ŝe wykonanie operacji za pośrednictwem parametru wskaźnikowego moŜe powodować
zmianę wartości zmiennej wskazywanej przez skojarzony z nim argument.
Parametry tablicowe
KaŜdy parametr tablicowy jest niejawnie zastępowany parametrem wskaźnikowym, który powstaje po
zastąpieniu deklaratora vec[i] deklaratorem (* const vec).
W szczególności funkcja
int sum(int tab[20])
{
int sum = 0;
for(int i = 0; i < 3 ; i++)
sum += tab[i];
return sum;
}
jest niejawnie przekształcana w funkcję
int sum(int *const tab)
{
int sum = 0;
for(int i = 0; i < 3 ; i++)
sum += tab[i];
return sum;
}
Powoduje to, Ŝe jeśli chce się zdefiniować funkcję do sumowania tablic, nie odwołującą się do zmiennych
globalnych, to jeden z jej argumentów musi określać liczbę elementów tablicy.
#include <iostream.h>
int sum(int tab[], int count)
{
int sum = 0;
for(int i = 0; i < count ; i++)
sum += tab[i];
return sum;
}
int main(void)
{
int small[] = { 10 },
large[] = { 10, 20, 30 };
cout << sum(small, 1) << endl;
cout << sum(large, 3) << endl;
return 0;
}
Parametry odnośnikowe
Z parametrem odnośnikowym moŜna skojarzyć argument, który jest takiego samego typu jak parametr, albo
który moŜna poddać niejawnej konwersji do typu parametru.
Zainicjowanie parametru polega na skopiowania odnośnika. Podobnie jak dla parametru wskaźnikowego, nie
pociąga to za sobą kopiowania zmiennej identyfikowanej przez argument.
Po dokonaniu skojarzenia, wszelkie operacje wykonywane na parametrze dotyczą zmiennej identyfikowanej
przez argument. MoŜe to powodować zmianę wartości skojarzonego z nim argumentu.
37
#include <iostream.h>
int main(void)
{
void inc(int &ref);
int fix = 10;
cout << fix << endl; // 10
inc(fix);
cout << fix << endl; // 11
return 0;
}
void inc(int &ref)
{
++ref;
}
Program potwierdza, Ŝe wykonanie operacji za pośrednictwem parametru odnośnikowego moŜe powodować
zmianę wartości skojarzonego z nim argumentu.
Parametry funkcji main
Funkcja główna moŜe być zadeklarowana jako bezparametrowa albo dwu-parametrowa. Jeśli jest
dwuparametrowa, to jej pierwszy parametr jest typu int i ma wartość równą liczbie argumentów programu
zwiększonej o 1, a drugi jest typu char *[] i jest tablicą odnośników do łańcuchów zainicjowanych nazwą
programu oraz nazwami jego argumentów.
#include <iostream.h>
int main(int argc, char *argv[])
{
cout << "My name is: " << argv[0] << endl;
cout << "My arguments are: " << endl;
for(int i = 1; i < argc ; i++)
cout << argv[i] << endl;
return 0;
}
Program wyprowadza swoją nazwę i argumenty; kaŜde w osobnym wierszu.
Skojarzenia powrotne
W chwili zakończenia wykonywania funkcji rezultatowej (o typie róŜnym od void) następuje skojarzenie jej
rezultatu z wyraŜeniem występującym w instrukcji powrotu. Odbywa się to według tych samych zasad co
skojarzenie parametru z argumentem i polega na zainicjowaniu rezultatu funkcji wyraŜeniem występującym w
instrukcji powrotu.
Uwaga: Rezultat funkcji jest zmienną. Typ rezultatu jest identyczny z typem funkcji. Nazwą rezultatu jest
wywołanie funkcji. Z punktu widzenia łączenia operacji (np. ++*fun(1,2)+3), nazwa funkcji jest zastępowana
nazwą rezultatu.
#include <iostream.h>
#include <math.h>
double sqr(double val)
{
return val * val;
}
38
int main(void)
{
double a, b;
cin >> a >> b;
cout << sqrt(sqr(a) + sqr(b)) << endl;
return 0;
}
Wywołanie sqr(a) jest nazwą rezultatu o wartości "kwadrat a", wywołanie sqr(b) jest nazwą rezultatu o wartości
"kwadrat b", a wyraŜenie sqrt(sqr(a) + sqr(b)) jest nazwą rezultatu o wartości "pierwiastek z sumy kwadratów a
i b".
Typ nie-odnośnikowy
Jeśli typ funkcji jest nie-odnośnikowy, to zainicjowanie rezultatu polega na skopiowaniu zmiennej, której nazwą
jest wyraŜenie występujące w instrukcji powrotu.
Jeśli typ wyraŜenia nie jest identyczny z typem funkcji, to wyraŜenie jest poddawane konwersji do typu rezultatu.
Zezwala się na niejawne wykonanie co najwyŜej jednej konwersji standardowej i jednej definiowanej.
#include <iostream.h>
double getSqr(int par);
int main(void)
{
cout << getSqr(3) << endl; // 9
return 0;
}
double getSqr(int par)
{
return par * par; // return double(par * par);
}
WyraŜenie par * par jest nazwą zmiennej typu int zainicjowanej daną o wartości 9.
PoniewaŜ typ rezultatu jest róŜny od typu tej zmiennej, więc zostanie zastosowana niejawna konwersja
standardowa z typu int do double.
Po zainicjowaniu rezultatu zmienną double(par * par), wywołanie getSqr(3) moŜna traktować nazwę rezultatu.
dla dociekliwych
#include <iostream.h>
struct Child {
char name[20];
int age;
};
Child isa = { "Isabel", 15 };
Child getOlder(Child child, int val);
void show(Child &child);
int main(void)
{
show(isa); // Isabel is 15
show(getOlder(isa, 2)); // Isabel is 17
show(isa); // Isabel is 15
39
return 0;
}
Child getOlder(Child child, int val)
{
child.age += val;
return child;
}
void show(Child &child)
{
cout << child.name << " is " <<
child.age << endl;
}
Wywołanie funkcji getOlder(isa, 2) powoduje skopiowanie struktury isa do lokalnej zmiennej funkcji getOlder.
Operacja child.age += val jest wykonywana na tej zmiennej lokalnej.
Wywołanie getOlder(isa, 2) jest nazwą zmiennej, do której skopiowano tę zmienną lokalną.
Typ odnośnikowy
Jeśli typ funkcji jest odnośnikowy, to zainicjowanie rezultatu polega na skopiowaniu odnośnika do tej zmiennej,
której nazwą jest wyraŜenie występujące w instrukcji powrotu. A zatem wywołanie funkcji jest nazwą tej
zmiennej.
#include <iostream.h>
int &refVal(void)
{
static int val = -1;
return ++val;
}
int main(void)
{
cout << refVal() << endl; // 0
cout << refVal() << endl; // 1
refVal() = 5;
cout << refVal() << endl; // 6
++refVal();
cout << refVal() << endl; // 9
return 0;
}
Wywołanie refVal() jest nazwą statycznej zmiennej val. A zatem kaŜda operacja wykonana na refVal() dotyczy
tej właśnie zmiennej.
dla dociekliwych
Jeśli typ wyraŜenia w instrukcji powrotu nie jest zgodny z typem funkcji, to typ funkcji musi być ustalony
(const), a ponadto musi istnieć niejawna konwersja z typu wyraŜenia do typu zgodnego z typem funkcji.
#include <iostream.h>
const int &refVal(double par)
{
return par * par; // return double(par * par);
}
40
int main(void)
{
cout << refVal(3) << endl;
return 0;
}
Uwaga: WyraŜenie zawarte w instrukcji powrotu moŜe tylko wówczas identyfikować zmienną lokalną funkcji,
gdy typ funkcji jest ustalony.
#include <iostream.h>
int &getInc(int par);
int main(void)
{
cout << getInc(3) << endl; // bł
ą
d
return 0;
}
int &getInc(int par)
{
return ++par;
}
Wywołanie getInc(3) jest nazwą lokalnej zmiennej par. PoniewaŜ po powrocie z funkcji getInc zmienna par juŜ
nie istnieje, więc odwołanie się do niej jest zabronione. W Visual C++ program wyprowadza liczbę 4.
Program moŜna poprawić, nadając mu postać
#include <iostream.h>
const int &getInc(int par);
int main(void)
{
cout << getInc(3) << endl; // 4
return 0;
}
const int &getInc(int par)
{
return ++par;
}
Deklarowanie funkcji
Deklaracja funkcji podaje jej identyfikator oraz określa typ funkcji oraz typy jej parametrów. Jeśli ponadto
podaje ciało funkcji, to jest jej definicją.
Funkcje bezrezultatowe
Funkcja, której typem jest void, jest funkcją bezrezultatową. Jej wywołanie kończy się w chwili wykonania
instrukcji powrotu nie zawierającej wyraŜenia, albo w chwili zakończenia wykonywania jej ciała.
void outDiv(int a, int b)
{
if(b == 0)
return;
cout << a / b;
}
41
Funkcje otwarte i zamknięte
Funkcja zadeklarowana ze specyfikatorem inline jest realizowana jako otwarta. W odróŜnieniu od funkcji
zamkniętej, ciało funkcji otwartej wstawia się w kaŜdym miejscu jej wywołania. Powoduje to przyspieszenie
wykonania programu, ale niekiedy wydłuŜa jego kod wynikowy.
Uwaga: Wystąpienie specyfikatora inline nie ma wpływu na skutek wykonania programu. Jeśli funkcja otwarta
zostanie uznana za zbyt skomplikowaną, to moŜe być zrealizowana jako zamknięta.
inline in sum(int a int b)
{
return a + b;
}
Funkcje przeciąŜone
Jeśli w pewnym zakresie są widoczne deklaracje dwóch lub większej liczby funkcji o takiej samej nazwie, ale
róŜniących się typami parametrów, to ogół takich funkcji stanowi wieloaspektową funkcję przeciąŜoną.
W miejscu wywołania funkcji przeciąŜonej wywołuje się ten z jej aspektów, do którego parametrów najlepiej
pasują podane argumenty. Ma to miejsce wówczas, gdy istnieje taki aspekt, Ŝe do kaŜdego z jego parametrów
podany argument pasuje nie gorzej niŜ do pozostałych, ale istnieje taki parametr, do którego jeden z argumentów
pasuje lepiej niŜ do pozostałych.
Uwaga: Jeśli argument nie pasuje do parametru dokładnie, to moŜe być poddany konwersji dopasowującej, ale
im konwersja ta jest bardziej złoŜona, tym dopasowanie pierwotnego argumentu uznaje się za gorsze.
#include <iostream.h>
void out(char par);
void out(int par);
int main(void)
{
out('a');
out(2);
out(2.0); // bł
ą
d (niejednoznaczno
ść
)
return 0;
}
void out(char par)
{
cout << par << endl;
}
void out(int par)
{
cout << par << endl;
}
Argument 'a' typu char najlepiej pasuje do parametru typu char, a argument 2 typu int najlepiej pasuje do
parametru typu int.
Argument 2.0 typu double pasuje równie dobrze do parametru typu char jak i do parametru typu int. PoniewaŜ
do Ŝadnego z nich nie pasuje najlepiej, więc wywołanie out(2.0) jest błędne.
Gdyby z programu usunięto dowolną z funkcji out, to wszystkie odwołania do out byłyby poprawne.
42
Argumenty domniemane
W deklaracji parametru funkcji moŜe wystąpić inicjator wyraŜeniowy określający domniemaną wartość
argumentu kojarzonego z tym parametrem.
int sum(int a, int b =0, int c =0);
Jeśli pewien parametr wyposaŜono w argument domniemany, to kaŜdy z następnych parametrów takŜe musi być
wyposaŜony w argument domniemany.
int sum(int a, int b =0, int c); // bł
ą
d
Z kaŜdym parametrem nie wyposaŜonym w argument domniemany musi być skojarzony jawny argument.
Końcowy zestaw argumentów, dla których podano domniemania, moŜna pominąć. W ich miejscu zostaną uŜyte
argumenty domniemane.
#include <iostream.h>
int sum(int a, int b, int c =0, int d =0);
int main(void)
{
cout << sum(1, 2, 3) << endl; // 6
cout << sum(1, 2) << endl; // 3
cout << sum(1) << endl; // bł
ą
d
return 0;
}
int sum(int a, int b, int c, int d)
{
return a + b + c + d;
}
dla dociekliwych
WyraŜenie określające wartość argumentu domniemanego nie musi być wyraŜeniem stałym. W takim wypadku
jest opracowywane w kontekście jego deklaracji, a nie w kontekście jego uŜycia.
#include <iostream.h>
int p = 20;
int sub(int a =p*p)
{
return a;
}
int main(void)
{
int p = 10;
cout << sub() << endl; // 400
::p = 10;
cout << sub() << endl; // 100
return 0;
}
Wywołania rekurencyjne
43
Wywołanie funkcji jest rekurencyjne, jeśli nastąpi przed powrotem z jej poprzedniego wywołania. UŜycie
rekurencji moŜe uczynić program czytelniejszym, ale w wielu wypadkach powoduje zwiększenie rozmiaru
pamięci operacyjnej niezbędnej do jego wykonania.
#include <iostream.h>
#include <limits.h>
#include <stdlib.h>
int sqrt(int par, int min =0, int max =INT_MAX)
{
int mid = (min + max) / 2;
if(mid == min)
return mid;
if(par < double(mid) * mid)
return sqrt(par, min, mid);
else
return sqrt(par, mid, max);
}
int main(void)
{
int val;
cin >> val;
val = abs(val);
cout << "sqrt(" << val << ") = " <<
sqrt(val) << endl;
return 0;
}
Funkcja sqrt dostarcza pierwiastek z jej nieujemnego argumentu. Nieobowiązkowe argumenty dodatkowe
określają przedział, w którym znajduje się pierwiastek.
Definiowanie funkcji
Zdefiniowanie funkcji polega na podaniu jej ciała. Dobry styl programowania poznaje się po uŜyciu wielu
krótkich, a nie małej liczby długich funkcji.
Tak dalece jak jest to moŜliwe, naleŜy posługiwać się funkcjami bibliotecznymi. Ilustruje to następujący
program, który napisano w dwóch wersjach: z uŜyciem i bez uŜycia funkcji bibliotecznych.
#include <iostream.h>
#include <iomanip.h>
#include <string.h>
const int Size = 100;
int main(void)
{
char srcOne[Size],
srcTwo[Size];
cin >> setw(Size) >> srcOne >>
setw(Size) >> srcTwo;
char trg[2*Size-1];
strcat(strcpy(trg, srcOne), " ");
int len = strlen(strcat(trg, srcTwo));
cout << trg << endl << len << endl;
return 0;
}
44
Program wprowadza dwa łańcuchy, łączy je oddzielając spacją, a następnie wyprowadza: łańuch docelowy,
długość łańcucha docelowego i wynik porównania łańcuchów źródłowych.
#include <iostream.h>
const int Size = 100;
int strLen(char *ptr);
char *strCpy(char *pTrg, char *pSrc);
char *strCat(char *pTrg, char *pSrc);
int strCmp(char *pOne, char *pTwo);
int main(void)
{
char srcOne[Size],
srcTwo[Size];
cin >> setw(Size) >> srcOne >>
setw(Size) >> srcTwo;
char trg[2*Size-1];
strCat(strCpy(trg, srcOne), " ");
int len = strLen(strCat(trg, srcTwo));
cout << trg << endl << len << endl;
cout << srcOne << ' ';
char chr = '=';
switch(strCmp(srcOne, srcTwo)) {
case +1:
chr = '>';
break;
case -1:
chr = '<';
break;
}
cout << chr << ' ' << srcTwo << endl;
return 0;
}
int strLen(char *ptr)
{
int len = 0;
while(*ptr++)
len++;
return len;
}
char *strCpy(char *pTrg, char *pSrc)
{
char *pTrg2 = pTrg;
while(*pTrg++ = *pSrc++);
return pTrg2;
}
char *strCat(char *pTrg, char *pSrc)
{
char *pTrg2 = pTrg;
strCpy(pTrg += strLen(pTrg), pSrc);
return pTrg2;
}
int strCmp(char *pOne, char *pTwo)
{
while(*pOne || *pTwo)
if(*pOne++ != *pTwo++)
if(pOne[-1] > pTwo[-1])
return +1;
else
return -1;
45
return 0;
}
46
Zarządzanie pamięcią
Wykonanie programu polega na przepływie sterowania przez jego deklaracje, definicje i instrukcje.
W pierwszej kolejności sterowanie przepływa przez wszystkie deklaracje globalne (takie, które nie wchodzą w
skład innych deklaracji). Następnie jest wyszukiwana funkcja główna i sterowanie przepływa przez zawarte w
niej instrukcje. Przepływ sterowania kończy się po powrocie z wywołania funkcji exit albo po wykonaniu
instrukcji powrotu z funkcji głównej.
#include <iostream.h>
#include <stdlib.h>
int main(void)
{
int num;
cin >> num;
if(num != 0) {
cout << num << endl;
exit(num);
}
return 0;
}
W zaleŜności od tego, jaką wartość ma wprowadzona liczba, program kończy się po napotkaniu instrukcji
powrotu albo po wywołaniu funkcji exit.
Zmienne statyczne
Jeśli sterowanie przepłynie przez definicję zmiennej globalnej, albo przez definicję zmiennej lokalnej
zadeklarowanej ze specyfikatorem static, to zostanie utworzona zmienna statyczna. TuŜ przed zakończeniem
wykonywania programu wszystkie zmienne statyczne zostaną zniszczone. Odbędzie się to w kolejności
odwrotnej do ich tworzenia.
Uwaga: Zmienna statyczna jest tworzona w obszarze statycznym. Inicjator zmiennej statycznej jest brany pod
uwagę tylko podczas pierwszego opracowania jej deklaracji.
#include <iostream.h>
int main(void)
{
void fun(int par);
fun(10);
static int one = 1;
fun(20);
return 0;
}
int two = 2;
void fun(int par)
{
static int loc = par;
cout << loc << ' ' << par << endl;
loc++;
47
}
Zmienne statyczne one, two, loc zostaną utworzone w kolejności: two, loc, one, a zostaną zniszczone w
kolejności: one, loc, two.
Program wyprowadzi dwie pary liczb: 10 10 i 11 20.
Zmienne automatyczne
Jeśli sterowanie przepłynie przez definicję zmiennej lokalnej, nie zadeklarowanej ze specyfikatorem static albo
extern, to zostanie utworzona zmienna automatyczna. Jawny albo niejawny inicjator zmiennej automatycznej
będzie brany pod uwagę podczas kaŜdego opracowania tej definicji.
Uwaga: Zmienne automatyczne tworzy się na stosie. Stos jest obszarem pamięci, w którym moŜna tworzyć
zmienne, ale takim, Ŝe moŜna je niszczyć tylko w kolejności odwrotnej do ich tworzenia.
void sub(void)
{
int num; // int num = int();
cout << num; // bł
ą
d
// ...
}
Zmienną automatyczną num wyposaŜono w niejawny inicjator = int() dostarczający wartość nieokreśloną.
Zmienna automatyczna zostanie zniszczona tuŜ przed zakończeniem wykonywania bloku (wnętrza instrukcji
grupującej), w którym ją zadeklarowano. Jeśli w bloku zadeklarowano więcej niŜ jedną zmienną automatyczną,
to ich niszczenie odbędzie się w kolejności odwrotnej do ich tworzenia, ale przed przystąpieniem do niszczenia
zmiennych statycznych.
#include <iostream.h>
int main(void)
{
int cnt = 2;
while(cnt > 0) {
int val = cnt--;
cout << val << endl;
}
return 0;
}
int one = 10;
Najpierw zostanie utworzona zmienna statyczna one, a po niej zmienna automatyczna cnt. Następnie zostanie
utworzona i zniszczona zmienna automatyczna val zainicjowana wartością 2, a po tym zostanie utworzona i
zniszczona zmienna automatyczna val zainicjowana wartością 1. TuŜ przed wykonaniem instrukcji powrotu
zostanie zniszczona zmienna cnt, a po niej zmienna one.
Zmienne kontrolowane
Zmienna kontrolowana powstaje na skutek wykonania operacji new, a jest niszczona po jawnym wykonaniu
operacji delete. Zmienne kontrolowane są tworzone na stercie. Sterta jest obszarem pamięci, do którego moŜna
dokładać zmienne, a następnie usuwać je w dowolnej kolejności.
48
Jeśli wykonanie operacji new jest niemoŜliwe, poniewaŜ wyczerpano obszar sterty, to rezultatem operacji
przydzielenia pamięci jest wskaźnik pusty (o wartości reprezentowanej przez 0).
Uwaga: Programiści rzadko badają rezultat operacji new, bo są z natury optymistami.
int *ptr = new char [10000000];
if(ptr == 0) {
cout << "No memory" << endl;
exit(-1);
}
Zmienne skalarne
Wykonanie operacji
new Type
w której Type jest opisem typu skalarnego (tj. nie-tablicowego!), powoduje utworzenie na stercie zmiennej typu
Type. Rezultatem operacji jest wskaźnik zainicjowany wskazaniem utworzonej zmiennej.
Wykonanie operacji
delete ptr
w której ptr wskazuje zmienną utworzoną na stercie, powoduje zniszczenie tej zmiennej.
#include <iostream.h>
int main(void)
{
int *pOne = new int;
double &two = *new double;
two = 2.8;
*pOne = (int)two;
cout << *pOne << endl; // 2
delete pOne;
delete &two;
return 0;
}
Najpierw zostanie utworzona zmienna typu int, a następnie zmienna typu double. Najpierw zostanie zniszczona
zmienna typu int, a następnie zmienna typu double.
Zmienne tablicowe
Wykonanie operacji
new Type
w której Type jest opisem typu tablicowego (np. int [12]), powoduje utworzenie na stercie zmiennej typu Type.
Rezultatem operacji jest wskaźnik zainicjowany wskazaniem zerowego elementu utworzonej tablicy.
Jeśli elementami tablicy są obiekty, to do ich zainicjowania jest niejawnie stosowany konstruktor domyślny.
Uwaga: WyraŜenie określające liczbę elementów tablicy nie musi być wyraŜeniem stałym.
Wykonanie operacji
delete [] ptr
49
w której ptr wskazuje zerowy element tablicy utworzonej na stercie, powoduje zniszczenie tej tablicy.
#include <iostream.h>
#include <string.h>
int main(void)
{
char *ptr = new char [100];
cin >> ptr;
char &vec = *new char [strlen(ptr) + 1];
cout << strcpy(&vec, ptr) << endl;
delete [] ptr;
delete [] &vec;
return 0;
}
Program tworzy na stercie 100-elementową tablicę znakową i wprowadza do niej ciąg znaków. Następnie tworzy
na stercie najmniejszą tablicę, w której moŜna pomieścić wprowadzony ciąg znaków oraz tworzy na stosie
odnośnik vec identyfikujący zerowy element tej tablicy.
Przed zakończeniem wykonywania program niszczy obie tablice, w kolejności ich utworzenia.
OstrzeŜenie
W Ŝadnym wypadku nie wolno zmiennej utworzonej za pomocą operacji new dla zmiennych skalarnych niszczyć
za pomocą operacji delete dla zmiennych tablicowych, a zmiennej utworzonej za pomocą operacji new dla
zmiennych tablicowych niszczyć za pomocą operacji delete dla zmiennych skalarnych.
Nie wolno takŜe uŜywać operacji delete ze wskaźnikiem ptr identyfikującym co innego niŜ zmienna skalarna
albo zerowy element tablicy utworzonej za pomocą operacji new, ani przyjmować, Ŝe po wykonaniu operacji
delete wskaźnik ptr ma wartość określoną.
Uwaga: W celu uniknięcia trudnych do wykrycia błędów, zaleca się (o ile to moŜliwe) zerowanie wskaźnika ptr
bezpośrednio po uŜyciu go w operacji delete.
#include <iostream.h>
int main(void)
{
int *ptr = new int [5];
delete [] (ptr + 2); // bł
ą
d
int &vec = *new int [5];
delete &vec; // bł
ą
d
int &ref = (*new int) = 3;
delete &ref;
cout << ref << endl; // bł
ą
d
return 0;
}
Mimo iŜ program jest poprawny składniowo, zawiera 3 powaŜne błędy logiczne. Wykonany w środowisku
Visual C++, program ten załamuje system zarządzania stertą.
50
Widoczność deklaracji
Identyfikatorem zmiennej, funkcji i typu moŜna posługiwać się tylko w miejscu, w którym jest widoczna jego
deklaracja.
Zaleca się, aby w tym samym zakresie, identyfikator uŜyty do zadeklarowania zmiennej, funkcji albo typu nie
został uŜyty do zadeklarowania innej zmiennej, funkcji albo typu.
Uwaga: Podano zalecenie, a nie zakaz, poniewaŜ w tym samym zakresie mogą wystąpić, nie kolidujące za sobą,
deklaracje funkcji i typu.
void id(int id)
{
struct id {
};
extern void id(id id);
int id = 10; // bł
ą
d
}
Z kaŜdą deklaracja jest związany jej zakres i zasięg. Jeśli w pewnym module zdefiniowano identyfikator o
zasięgu globalnym, a w innym zadeklarowano go ze specyfikatorem extern, to oba dotyczą tej samej zmiennej,
funkcji albo typu.
plik Main.cpp
#include <iostream.h>
int fix = 10; // definicja
int main(void)
{
extern void fun(void); // deklaracja
fun();
return 0;
}
plik One.cpp
#include <iostream.h>
void fun() // definicja
{
extern int fix; // deklaracja
cout << fix << endl; // 10
}
Gdyby pominięto wszystkie specyfikatory extern, to program stałby się statycznie poprawny, ale dynamicznie
błędny. Błąd polegałby na uŜyciu wartości zmiennej, której nie zainicjowano.
Deklaracje lokalne
Zakresem deklaracji identyfikatora zadeklarowanego w bloku jest obszar programu od punktu zadeklarowania
do końca bloku. Zasięgiem deklaracji jest ta część zakresu, która nie jest zakresem innej deklaracji takiego
samego identyfikatora.
51
#include <iostream.h>
int main(void)
{
int num = 10;
cout << num << endl; // 10
{
cout << num << endl; // 10
int num = 20;
cout << num << endl; // 20
}
cout << num << endl; // 10
return 0;
}
Zakresem deklaracji pierwszej zmiennej num jest obszar zaczynający się od = 10 i kończący na klamrze
zamykającej funkcję main.
Zakresem deklaracji drugiej zmiennej num jest obszar zaczynający się od = 20 i kończący na klamrze
zamykającej blok wewnętrzny.
Zasięgiem deklaracji pierwszej zmiennej num jest zakres deklaracji pierwszej zmiennej num, pomniejszony o
zakres deklaracji drugiej zmiennej num.
Deklaracje globalne
Zakresem deklaracji identyfikatora zadeklarowanego w module (tj. poza blokiem), jest obszar programu od
punktu zadeklarowania do końca modułu. Zasięgiem deklaracji jest ta część zakresu, która nie jest zakresem
innej deklaracji takiego samego identyfikatora.
Uwaga: Modułem jest zawartość pliku *.cpp projektu, po zastosowaniu uŜytych w nim dyrektyw (#include, #if,
#endif, itp.).
#include <iostream.h>
int num = 10;
int main(void)
{
cout << num << endl; // 10
{
cout << num << endl; // 10
int num = 20;
cout << num << endl; // 20
}
cout << num << endl; // 10
return 0;
}
int num2 = num;
Zasięg deklaracji pierwszego identyfikatora num obejmuje m.in. deklarację występującą po funkcji main.
Deklaracje i definicje
52
Jeśli deklaracja globalna zawiera specyfikator static, to jest widoczna tylko w jej module. Jeśli deklaracja
globalna jest definicją, ale nie zawiera specyfikatora static, to jest widoczna w tych obszarach pozostałych
modułów programu, w których jest widoczna zgodna z nią deklaracja ze specyfikatorem extern bez inicjatora,
nie dotycząca deklaracji globalnej ze specyfikatorem static.
Uwaga: Globalne zmienne ustalone są domyślnie wyposaŜone w specyfikator static. Specyfikator extern
występujący w deklaracji funkcji moŜna pominąć.
plik Main.cpp
#include <iostream.h>
int main(void)
{
int fun(void); // pomini
ę
to extern
cout << fun() << endl; // 10
extern int num;
cout << num << endl; // 20
return 0;
}
plik One.cpp
static int num = 10;
int fun(void)
{
extern int num; // zb
ę
dne
return num;
}
plik Two.cpp
int num = 20;
Deklaracje typów
Globalna deklaracja typu, na przykład
struct Child;
nie wystarczy do tego, aby moŜna było nawiązać do definicji tego typu podanej w innym module.
W odróŜnieniu od definicji zmiennej i funkcji, która w zbiorze modułów programu moŜe wystąpić tylko jeden
raz, definicja struktury musi być powtórzona w kaŜdym z odwołujących się do niej modułów.
plik Main.cpp
#include <iostream.h>
struct Child {
char name[20];
int age;
};
int main(void)
{
Child getIsa(void);
Child isa = getIsa();
53
cout << isa.name << " is " <<
isa.age << endl;
return 0;
}
plik Isa.cpp
struct Child {
char name[20];
int age;
};
Child isa = { "Isabel", 15 };
Child getIsa(void)
{
return isa;
}
albo lepiej i bezpieczniej
plik child.h
struct Child {
char name[20];
int age;
};
plik Main.cpp
#include <iostream.h>
#include "child.h"
int main(void)
{
Child getIsa(void);
Child isa = getIsa();
cout << isa.name << " is " <<
isa.age << endl;
return 0;
}
plik Isa.cpp
#include "child.h"
Child isa = { "Isabel", 15 };
Child getIsa(void)
{
return isa;
}
54
Studia programowe
Przedstawiono dwa rozwiązania następującego problemu
Napisać program, który wprowadza z pliku sekwencję danych arytmetycznych, a następnie wyprowadza
ich średnie odchylenie standardowe: pierwiastek z sumy kwadratów róŜnic dana-średnia, podzielony
liczbę danych.
W szczególności, jeśli w pliku Data.txt umieści się liczby 6 9 12, a jako argument programu poda Data.txt
(polecenie Project / Settings // Debug), to nastąpi wyprowadzenie liczby 1.41421.
Struktura tablicowa
#include <iostream.h>
#include <fstream.h>
#include <math.h>
int readData(char *fileName, double *&pData);
double getAverage(double *pData, int count);
double getResult(double *pData, int count, double average);
void freeMemory(double *pData);
int main(int noOfArgs, char *pArg[])
{
if(noOfArgs != 2) {
cout << "Usage is: " << pArg[0] <<
" fileName" << endl;
return -1;
}
double *pData;
char *fileName = pArg[1];
int count = readData(fileName, pData);
if(count) {
double average = getAverage(pData, count);
double result = getResult(pData, count, average);
cout << "Result = " << result << endl;
} else
cout << "Error!" << endl;
return 0;
}
int readData(char *fileName, double *&pData)
{
const int start = 200;
ifstream inp;
inp.open(fileName, ios::in | ios::nocreate);
int count = 0;
if(inp.is_open()) {
pData = new double [start];
int len = start;
double tmp;
while(tmp = 0, inp >> tmp, tmp) {
if(count == len) {
double *ptr = new double [len *= 2];
for(int j = 0; j < len /2 ; j++)
ptr[j] = pData[j];
delete [] pData;
pData = ptr;
}
pData[count++] = tmp;
}
}
return count;
}
55
double getAverage(double *pData, int count)
{
double sum = 0;
for(int i = 0; i < count ; i++)
sum += pData[i];
return sum / count;
}
double getResult(double *pData, int count, double average)
{
double sumSqr = 0;
for(int i = 0; i < count ; i++) {
double dif = pData[i] - average;
sumSqr += dif * dif;
}
return sqrt(sumSqr) / count;
}
void freeMemory(double *pData)
{
delete [] pData;
}
Struktura listowa
#include <iostream.h>
#include <fstream.h>
#include <math.h>
struct Item {
Item *pNext;
double value;
};
struct List {
Item *pFirst;
int count;
};
List list = { 0 };
int readData(char *fileName, List &list);
double getAverage(List &list);
double getResult(List &list, double average);
void freeMemory(List &list);
int main(int noOfArgs, char *pArg[])
{
if(noOfArgs != 2) {
cout << "Usage is: " << pArg[0] <<
" fileName" << endl;
return -1;
}
char *fileName = pArg[1];
int count = readData(fileName, list);
if(count) {
double average = getAverage(list);
double result = getResult(list, average);
cout << "Result = " << result << endl;
} else
cout << "Error!" << endl;
freeMemory(list);
return 0;
}
int readData(char *fileName, List &list)
{
ifstream inp;
inp.open(fileName, ios::in | ios::nocreate);
56
int count = 0;
if(inp.is_open()) {
double tmp;
while(tmp = 0, inp >> tmp, tmp) {
Item *pItem = new Item;
pItem->pNext = list.pFirst;
pItem->value = tmp;
list.pFirst = pItem;
count++;
}
}
return list.count = count;
}
double getAverage(List &list)
{
double sum = 0;
Item *pItem = list.pFirst;
while(pItem) {
sum += pItem->value;
pItem = pItem->pNext;
}
return sum / list.count;
}
double getResult(List &list, double average)
{
double sumSqr = 0;
Item *pItem = list.pFirst;
while(pItem) {
double dif = pItem->value - average;
sumSqr += dif * dif;
pItem = pItem->pNext;
}
return sqrt(sumSqr) / list.count;
}
void freeMemory(List &list)
{
Item *pItem = list.pFirst, *pTmp;
while(pItem) {
pTmp = pItem->pNext;
delete pItem;
pItem = pTmp;
}
}
57
Dodatek A
Priorytety operatorów
Operatory wyszczególniono w kolejnoœci malej¹cego priorytetu.
Wi¹zanie Operator
prawe
::
lewe
Type::
lewe
[] . -> () Type()
lewe
++ -- (nastêpnikowe)
prawe
++ -- (poprzednikowe)
prawe
sizeof + - ~ ! & * new delete (Type) throw
lewe
.* ->*
lewe
* / %
lewe
+ -
lewe
<< >>
lewe
< <= > >=
lewe
== !=
lewe
&
lewe
^
lewe
|
lewe
&&
lewe
||
prawe
?:
prawe
= *= /= %= += -= <<= >>= &= ^= |=
lewe
,
l-nazwą zmiennej (por. Dodatek B) jest tylko: operacja przypisania (np. a+=b), przedrostkowego zwiększenia
(np. ++a), przedrostkowego zmniejszenia (np. --a), indeksowania (np. ptr[i]), wyłuskania (np. *ptr), wyboru
(np. str.f i ptr->f), warunku którego dwa ostatnie argumenty są l-nazwami (np. a>0?a:b), konwersji do typu
odnośnikowego (np. (int &)a) oraz globalności (np. ::) i zakresu (np. Child::name).
58
Dodatek B
Opracowywanie wyraŜeń
WyraŜenia są zapisami operacji. O kolejności wykonywania operacji decyduje sposób uŜycia nawiasów oraz
uwzględnienie priorytetów i wiązań operatorów (por. Dodatek A).
Jeśli kilka operatorów zapisano spójnie (tj. bez odstępów), wówczas za pierwszy uznaje się najdłuŜszy. A
zatem: poniewaŜ w C++ istnieją operatory + i ++, ale nie istnieje operator +++, więc wyraŜenie
a +++ b
jest traktowane jak
(a++) + b // a nie jak: a + (++b)
Priorytety
PoniewaŜ w C++ priorytet mnoŜenia jest wyŜszy niŜ priorytet dodawania, więc wyraŜenie
a + b * c
jest traktowane jak
a + (b * c) // a nie jak: (a + b) * c
Podobnie, poniewaŜ w C++ priorytet następnikowej operacji zwiększenia (++) jest wyŜszy niŜ priorytet operacji
wyłuskania (*), więc wyraŜenie
*ptr++
jest traktowane jak
*(ptr++) // a nie jak: (*ptr)++
Wiązania
PoniewaŜ w C++ priorytet odejmowania (-) jest równy priorytetowi dodawania (+), więc jeśli pewnego
podwyraŜenia dotyczą oba takie operatory, to odwołanie się do priorytetów nie wystarcza i trzeba odwołać się do
wiązań.
PoniewaŜ w C++ wiązanie operacji odejmowania i dodawania jest lewe, więc wyraŜenia
a - b + c
cout << a << b
są traktowane jak
(a - b) + c // a nie jak a - (b + c)
59
(cout << a) << b // a nie jak: cout << (a << b)
(środkowe podwyraŜenia dowiązano do lewej).
Dla porównania, poniewaŜ wiązanie operacji przypisania jest prawe, więc wyraŜenie
a = b = c
jest traktowane jak
a = ( b = c) // a nie jak: (a = b) = c
Kolejność
Kolejność opracowywania argumentów operacji jest nieokreślona. Dotyczy to zarówno argumentów wywołania
funkcji, jak i argumentów operacji dwuargumentowych, takich jak przypisanie.
Dlatego zaleca się, aby w wyraŜeniu, w którym następuje zmiana wartości zmiennej, nie odwoływano się
(dodatkowo!) do tej zmiennej.
fun(cout << 100, cout << 200);
int tab[4] = { 10, 20, 30 },
pos = 1;
tab[pos] = ++pos;
Nie wiadomo, czy przed wykonaniem ciała funkcji fun zostanie wyprowadzona liczba 100 czy 200. W Visual
C++ zostanie wyprowadzona liczba 200.
Nie wiadomo, czy przypisanie dotyczy elementu tab[1] czy elementu tab[2]. W Visual C++ dotyczy ono tab[2].
Promocja
Niektóre operacje są wykonywane dopiero po promocji argumentu. Dotyczy to w szczególności zmiennych typu
char (poddawanych promocji do typu int).
char chr = 'a';
char &ref1 = chr;
char &ref2 = +chr; // bł
ą
d
char &ref3 = 'a'; // bł
ą
d
Typ wspólny
Jeśli argumenty operacji są róŜnych typów, to wykonuje się ją w ich typie wspólnym. W szczególności typem
wspólnym dla char i int jest int, a typem wspólnym dla double i int jest double.
Uwaga: Jeśli wyraŜenie jest pewnego typu, to nie oznacza to, Ŝe wszystkie jego operacje wykonuje się w tym
typie.
#include <iostream.h>
#include <limits.h>
int main(void)
{
int max = INT_MAX;
cout << max * max << endl; // 1 (sic!)
cout << 0.0 + max * max << endl; // 1 (sic!)
cout << double(max) * max << endl; // ok. 4.6e18
60
return 0;
}
Mimo iŜ typem wyraŜenia zawierającego liczbę 0.0 jest double, iloczyn max * max jest obliczany w typie int.
Punkty charakterystyczne
Punktem charakterystycznym jest miejsce w programie, w którym realizuje się wszystkie "zaległe" skutki
uboczne, takie jak operacje wejścia-wyjścia i przypisania.
Punkt charakterystyczny występuje m.in. po kaŜdym kompletnym wyraŜeniu, przed kaŜdym średnikiem, przed
pierwszą instrukcją funkcji oraz przed operatorami koniunkcji i dysjunkcji.
Programy zaleŜne od połoŜenia punktu charakterystycznego naleŜy konstruować ze szczególną ostroŜnością.
int fix = 10;
++fix = fix;
cout << fix;
PoniewaŜ operacja zwiększenia (++) moŜe być zrealizowana dopiero w punkcie charakterystycznym, więc nie
wiadomo, czy zostanie wyprowadzona liczba 10 czy 11. W Visual C++ zostanie wyprowadzona liczba 11.
Nazwy
KaŜde wyraŜenie i podwyraŜenie (w szczególności zapis operacji), moŜna rozpatrywać jako nazwę pomocniczej
zmiennej tymczasowej. Podczas opracowywania wyraŜenia, kaŜdą z operacji zastępuje się nazwą jej rezultatu.
Uwaga: Pomocniczą zmienną tymczasową niszczy się bezpośrednio po opracowaniu kompletnego wyraŜenia,
którego opracowania wymagało utworzenia tej zmiennej.
W szczególności, jeśli przyjąć, Ŝe zmiennymi tymczasowymi są t1, t2 i t3 to instrukcja
cout << 1 + 2 * 3;
jest wykonywana tak, jak
int t1, t2, t3;
t1 = 2 * 3, t2 = 1 + t1, cout << t2
a zmienne tymczasowe zostaną zniszczone w chwili, gdy sterowanie "przepłynie przez średnik".
l-nazwy
Przyjmuje się z definicji, Ŝe l-nazwą jest tylko: identyfikator zmiennej nie-ustalonej, rezultat funkcji o typie
odnośnikowym oraz rezultat operacji wymienionych w Dodatku A. Nie jest l-nazwą literał, ani wskaźnik
powstały z niejawnego przekształcenia nazwy tablicy.
Posługując się taką definicją moŜna podać następujące wymagania
1) Odnośnik do zmiennej nie-ustalonej moŜe być zainicjowany tylko takim wyraŜeniem, które jest l-nazwą
zmiennej.
np.
const int fix1 = 10;
int &fix2 = 20; // bł
ą
d
2) Argumentem operacji zwiększenia (++), zmniejszenia (--), wskazywania (&) i wyboru (. i ->) moŜe być tylko
takie wyraŜenie, które jest l-nazwą zmiennej.
61
np.
int fix = 10;
++(int)fix; // bł
ą
d
fix++++; // bł
ą
d
int *ptr = &20; // bł
ą
d
3) Lewym argumentem przypisania (=, += itp.) moŜe być tylko takie wyraŜenie, które jest l-nazwą zmiennej.
np.
int fix = 10;
fix++ = 20; // bł
ą
d
int tab[] = { 10 };
tab = 20; // bł
ą
d
Uwaga: Niepoprawność operacji fix++++ wynika stąd, Ŝe fix++ nie jest l-nazwą, a więc nie moŜe być
argumentem ponownej operacji zwiększenia.
62
Dodatek C
Konwersje standardowe
Konwersją standardową, jest taka predefiniowana konwersja, która moŜe być wstawiona do programu
niejawnie.
Konwersjami standardowymi są m.in.:
0) Przekształcenie promocyjne (np. zmiennej typu char w zmienna typu int).
1) Przekształcenie zmiennej arytmetycznej albo wskaźnika w orzecznik (np. zmiennej typu int w zmienną typu
bool).
2) Przekształcenie zmiennej arytmetycznej w zmienną arytmetyczną innego typu (np. zmiennej typu double w
zmienną typu int).
3) Przekształcenie nazwy tablicy na wskaźnik do jej zerowego elementu.
4) Przekształcenie nazwy zmiennej na odnośnik do tej zmiennej.
5) Przekształcenie wskaźnika do obiektu na wskaźnik do jego podobiektu.
6) Przekształcenie odnośnika do obiektu na odnośnik do jego podobiektu.
7) Przekształcenie wskaźnika do zmiennej na wskaźnik lokalizujący tę zmienną.
Nie są nimi m.in.
1) Przekształcenie wskaźnika do elementu tablicy na wskaźnik do tej tablicy.
2) Przekształcenie wskaźnika do tablicy na wskaźnik do jej elementu.
3) Przekształcenie wskaźnika do podobiektu na wskaźnik do jego obiektu.
4) Przekształcenie odnośnika do podobiektu na odnośnik do jego obiektu.
5) Przekształcenie wskaźnika lokalizującego zmienną na wskaźnik do tej zmiennej.
Uwaga: Poza konwersjami standardowymi, niejawne moŜe być zastosowany jedynie konstruktor i konwerter.
63
Dodatek D
Operatory bitowe
Operatorami bitowymi są: ~ (zanegowanie bitów), & (iloczyn bitów), | (suma bitów), ^ (suma modulo 2 bitów),
<< (przesunięcie bitów w lewo), >> (przesunięcie bitów w prawo).
Podczas wykonywania operacji na bitach przydatne okazują się literały szesnastkowe. Literał szesnastkowy ma
postać 0xh, w której h jest spójnym ciągiem cyfr szesnastkowych (0-9 i a-f).
cout << 0x12; // 18
cout << 0xffff; // 65535
Operator ~
Operacja zanegowania bitów ma postać
~exp
w której exp jest wyraŜeniem całkowitym.
Rezultatem operacji zanegowania bitów jest zmienna tymczasowa takiego samego typu jak zmienna exp, po
poddaniu jej promocjom, a następnie zanegowaniu kaŜdego jej bitu.
Uwaga: Negacją bitu 1 jest bit 0, a negacją bitu 0 jest bit 1.
int red = 1, green = 2, blue = 4;
int hue = red | green; // ... 011 (kolor
Ŝ
ółty)
hue = ~hue; // ... 100 (kolor niebieski)
Trzy najmniej znaczące bity zmiennej hue reprezentują jeden z 8 kolorów. Wykonanie operacji zanegowania
bitów powoduje zmianę koloru na dopełniający.
Operator &
Operacja iloczynu bitów ma postać
expL & expR
w której expL i expR są wyraŜeniami całkowitymi.
W celu utworzenia wyniku operacji, zmienne expL i expR poddaje się konwersjom do typu wspólnego, a
następnie kaŜdy bit wyniku tworzy się z odpowiadających sobie bitów argumentów wyznaczając ich iloczyn
logiczny.
Uwaga: Iloczyn logiczny pary bitów ma wartość 1 tylko wówczas gdy oba bity są jedynkowe.
int fix = 6; // 00 ... 110
const int mask = '\x3'; // 00 ... 011
fix &= ~mask;
cout << Fix; // 4 (00 ... 100)
64
Wykonanie operacji na zmiennej fix powoduje wyzerowanie tych wszystkich jej bitów, które w mask są
jedynkowe.
Operator ^
Operacja sumy modulo 2 bitów ma postać
expL ^ expR
w której expL i expR są wyraŜeniami całkowitymi.
W celu utworzenia wyniku operacji, zmienne expL i expR poddaje się konwersjom do typu wspólnego, a
następnie kaŜdy bit wyniku tworzy się z odpowiadających sobie bitów argumentów wyznaczając ich sumę
logiczną modulo 2.
Uwaga: Suma logiczna modulo 2 pary bitów ma wartość 1 tylko wówczas gdy bity są róŜne.
int fix = 6; // 00 ... 110
const int mask = '\x3'; // 00 ... 011
fix ^= mask;
cout << fix; // 5 (00 ... 101)
Wykonanie operacji na zmiennej fix powoduje zanegowanie tych wszystkich jej bitów, które w mask są
jedynkowe.
Operator |
Operacja sumy bitów ma postać
expL | expR
w której expL i expR są wyraŜeniami całkowitymi.
W celu utworzenia wyniku operacji, zmienne expL i expR poddaje się konwersjom do typu wspólnego, a
następnie kaŜdy bit wyniku tworzy się z odpowiadających sobie bitów argumentów wyznaczając ich sumę
logiczną.
Uwaga: Suma logiczna pary bitów ma wartość 0 tylko wówczas gdy oba bity są zerowe.
int fix = 5; // 00 ... 101
const int mask = '\x3'; // 00 ... 011
fix |= mask;
cout << Fix; // 7 (00 ... 111)
Wykonanie operacji na zmiennej fix powoduje ustawienie tych wszystkich jej bitów, które w mask są jedynkowe.
Operator <<
Operacja przesunięcia bitów w lewo ma postać
expL << n
w której expL i n są wyraŜeniami całkowitymi.
W celu utworzenia wyniku operacji, zmienną expL poddaje się promocji, a następnie kaŜdy bit wyniku tworzy
się z bitów tej nowej zmiennej po przesunięciu ich o n pozycji w lewo.
65
Uwaga: Podczas przesuwania w lewo bity najbardziej znaczące są odrzucane, a na pozycje najmniej znaczące
wchodzą bity 0.
int fix = 7; // 00 ... 0111
fix <<= 2;
cout << fix; // 28 (00 ... 011100)
Bity zmiennej fix przesunięto o 2 pozycje w lewo.
Operator >>
Operacja przesunięcia bitów w prawo ma postać
expL >> n
w której expL i n są wyraŜeniami całkowitymi.
W celu utworzenia wyniku operacji, zmienną expL poddaje się promocji, a następnie kaŜdy bit wyniku tworzy
się z bitów tej nowej zmiennej po przesunięciu ich o n pozycji w prawo.
Uwaga: Podczas przesuwania w prawo bity najmniej znaczące są odrzucane.
int fix = 15; // 00 ... 01111
fix >>= 2;
cout << fix; // 3 (00 ... 011)
Bity zmiennej fix przesunięto o 2 pozycje w prawo.
66
Dodatek E
Operacje wejścia-wyjścia
Większość operacji wejścia-wyjścia moŜna wykonać za pomocą operatorów. Do specjalnych celów przydają się
niekiedy funkcje wejścia-wyjścia.
Funkcje get i put
inp.get(chr)
Wprowadza ze strumienia inp najbliŜszy znak (w tym znak odstępu) i jego kod przypisuje zmiennej chr typu
char. Dostarcza odnośnik do inp.
out.put(chr)
Wyprowadza do strumienia out znak o kodzie chr. Dostarcza odnośnik do out.
#include <iostream.h>
#include <fstream.h>
#include <string.h>
int main(void)
{
ifstream inp;
inp.open("Data.txt", ios::in);
if(!inp.is_open())
return -1;
char chr;
while(inp.get(chr))
cout.put(chr);
return 0;
}
Program kopiuje na konsolę zawartość pliku Data.txt. Kopiowanie odbywa się znak-po-znaku.
Funkcje read i write
inp.read(ptr, len)
Wprowadza ze strumienia inp ciąg len najbliŜszych znaków i ich kody umieszcza w tablicy znakowej o
elemencie wskazywanym przez ptr. Dostarcza odnośnik do inp.
out.write(ptr, len)
Wyprowadza do strumienia out ciąg len znaków z tablicy znakowej, począwszy od elementu wskazywanego
przez ptr. Dostarcza odnośnik do out.
inp.gcount()
67
Dostarcza liczbę znaków wprowadzonych za pomocą ostatnio wywołanej funkcji read albo getline.
#include <iostream.h>
#include <fstream.h>
const int Size = 10;
int main(void)
{
ifstream inp;
inp.open("Data.txt", ios::in);
if(!inp.is_open())
return -1;
char buf[Size];
while(true) {
inp.read(buf, Size);
int len = inp.gcount();
if(len > 0)
cout.write(buf, len);
if(len < Size)
break;
}
return 0;
}
Program kopiuje na konsolę zawartość pliku Data.txt. Kopiowanie odbywa się porcjami po Size znaków.
Funkcja getline
inp.getline(ptr, len)
Wprowadza ze strumienia inp jeden wiersz, ale nie więcej niŜ len-1 najbliŜszych znaków, a ich kody, bez kodu
'\n', ale z dodatkowym kodem 0, umieszcza w tablicy znakowej o elemencie wskazywanym przez ptr. Dostarcza
odnośnik do inp.
#include <iostream.h>
#include <fstream.h>
const int Size = 100;
int main(void)
{
ifstream inp;
inp.open("Data.txt", ios::in);
if(!inp.is_open())
return -1;
char buf[Size];
while(inp) {
inp.getline(buf, Size);
int len = inp.gcount();
if(len > 0)
cout << buf << endl;
}
return 0;
}
Program kopiuje na konsolę zawartość pliku Data.txt. Kopiowanie odbywa się wierszami.
Funkcje peek i putback
inp.peek()
Dostarcza kod najbliŜszego znaku strumienia inp, ale znaku ze strumienia nie wprowadza (sic!).
68
inp.putback(chr)
Cofa do strumienia inp znak o kodzie chr. Dostarcza odnośnik do inp.
#include <iostream.h>
#include <fstream.h>
#include <ctype.h>
const int Size = 100;
int main(void)
{
ifstream inp;
inp.open("Data.txt", ios::in);
if(!inp.is_open())
return -1;
while(inp) {
char chr;
inp >> chr;
inp.putback(chr);
if(chr == '-' || chr == '+' || isdigit(chr)) {
double num;
inp >> num;
cout << num << endl;
} else {
char buf[Size];
inp >> buf;
cout << buf << endl;
}
}
return 0;
}
Program wprowadza z pliku Data.txt zawarte w nim liczby i łańcuchy, a następnie wyprowadza je na konsolę,
kaŜdy w osobnym wierszu.
Funkcje tellg i tellp
Funkcje tellg i tellp słuŜą do określania pozycji pliku. Pozycja jest daną typu streampos. W Visual C++ typ
streampos jest identyczny z typem int.
inp.tellg()
Dostarcza bieŜącą pozycję pliku otwartego w trybie ios::in.
inp.tellp()
Dostarcza bieŜącą pozycję pliku otwartego w trybie ios::out.
#include <iostream.h>
#include <fstream.h>
int main(void)
{
ifstream inp;
inp.open("C:\\config.sys", ios::in);
if(!inp.is_open())
return -1;
char chr;
while(inp >> chr)
;
streampos pos = inp.tellg();
cout << "Size = " << pos << endl;
return 0;
}
69
Program wyznacza rozmiar pliku config.sys.
Funkcje seekg i seekp
Funkcje seekg i seekp słuŜą do ustawiania pozycji pliku. Nowa pozycja pliku moŜe być podana względem
początku pliku (ios::beg), względem pozycji bieŜącej (ios::cur), albo względem pozycji końcowej (ios::end).
inp.seekg(pos) // inp.seekg(pos, ios::beg)
inp.seekg(pos, from)
Ustawia plik otwarty w trybie ios::in w pozycji pos, liczonej względem from (ios::beg, ios::cur, ios::end).
inp.seekp(pos) // inp.seekp(pos, ios::beg)
inp.seekp(pos, from)
Ustawia plik otwarty w trybie ios::out w pozycji pos, liczonej względem from (ios::beg, ios::cur, ios::end).
inp.seek(0);
Instrukcja ustawia strumień w pozycji początkowej.
Funkcja clear
Funkcja clear słuŜy do ustawienia stanu strumienia.
str.clear()
str.clear(ios::badbit)
Wywołanie bezargumentowe ustawia strumień str w stan dobry. Wywołanie z argumentem ios::badbit ustawia
go w stan zły.
#include <iostream.h>
#include <fstream.h>
int main(void)
{
ifstream inp;
inp.open("C:\\autoexec.bat", ios::in);
if(!inp.is_open())
return -1;
char chr;
for(int i = 0; i < 3 ; i++) {
while(inp.get(chr))
cout << chr;
inp.clear();
inp.seekg(0);
}
return 0;
}
Program ma na celu 3-krotne wyprowadzenie na konsolę zawartości pliku autoexec.bat.
PoniewaŜ po zakończeniu instrukcji while strumień inp znajduje się w stanie nie-dobrym, więc naleŜy ustawić go
w stan dobry. W przeciwnym razie wszystkie operacje wejścia-wyjścia dotyczące tego strumienia byłyby
pomijane, a zawartość pliku zostałaby wyprowadzona tylko 1 raz.
Operacje w pamięci
70
Operacje wejścia-wyjścia mogą dotyczyć nie tylko plików, ale równieŜ pamięci operacyjnej. Do wykonywania
operacji w pamięci słuŜą obiekty klas istrstream i ostrstream, zadeklarowanych w pliku nagłówkowym
strstream.h.
Argumentem konstruktora klasy istrstream jest wskaźnik łańcucha. Argumentami konstruktora klasy ostrstream
jest wskaźnik elementu tablicy znakowej i maksymalna liczba jej elementów, które mogą być uŜyte w operacji
wyjścia.
Uwaga: Operacja wyjścia nie zapisuje znaku końca łańcucha. NaleŜy to wykonać jawnie, na przykład za pomocą
symbolu ends.
#include <iostream.h>
#include <strstream.h>
int main(void)
{
char data[] = "10 20 30";
istrstream(data) >> a >> b >> c;
char buf[100];
ostrstream(buf, sizeof(buf)) << "Sum = " <<
a + b + c << ends;
cout << buf << endl;
return 0;
}
Przetwarzanie wyrywkowe
Wyrywkowo przetwarza się zazwyczaj pliki binarne. Plik binarny otwiera się w trybie
ios::in | ios::out | ios::binary
Operacje na pliku wykonuje się za pomocą funkcji read i write.
#include <iostream.h>
#include <fstream.h>
const char *const SrcName = "Data.txt";
const int Size = sizeof(int);
const char *const TrgName = "Random";
int main(void)
{
ifstream inp;
inp.open(SrcName, ios::in | ios::nocreate);
if(!inp.is_open()) {
cout << "Source failure" << endl;
return -1;
}
ofstream out;
out.open(TrgName, ios::out | ios::binary);
if(!out.is_open()) {
cout << "Target failure" << endl;
return -2;
}
// wprowadzanie
cout << endl << "reading ... " << endl;
int count = 0, tmp;
while(inp >> tmp) {
count++;
cout << tmp << endl;
out.write((char *)&tmp, Size);
71
}
out.close();
inp.close();
if(count == 0) {
cout << "No data" << endl;
return -4;
}
// sprawdzanie
cout << endl << "checking ... " << endl;
inp.open(TrgName, ios::in | ios::binary | ios::nocreate);
if(!inp.is_open()) {
cout << "Check failure" << endl;
return -5;
}
while(inp.read((char *)&tmp, Size))
cout << tmp << endl;
inp.close();
// sortowanie
cout << endl << "sorting ... ";
fstream rio;
rio.open(TrgName, ios::in | ios::out | ios::binary);
if(!rio.is_open()) {
cout << "Sort failure" << endl;
return -6;
}
bool sorted = false;
while(!sorted) {
cout << endl;
sorted = true;
for(int i = 0; i < count-1 ; i++) {
rio.seekp(i * Size, ios::beg);
if(!rio)
goto Exit;
int num1, num2;
rio.read((char *)&num1, Size).
read((char *)&num2, Size);
cout << num1 << " " << num2 << endl;
if(num2 < num1) {
rio.seekp(-2 * Size, ios::cur);
rio.write((char *)&num2, Size).
write((char *)&num1, Size);
sorted = false;
}
}
}
Exit:;
if(!sorted) {
cout << "Seek error" << endl;
return -7;
}
// wyprowadzanie
cout << endl << "showing ... " << endl;
rio.seekp(0, ios::beg);
for(int i = 0; i < count ; i++) {
rio.read((char *)&tmp, Size);
cout << tmp << endl;
}
return 0;
}
Program tworzy plik binarny, do którego zapisuje dane pochodzące pliku Data.txt. Następnie dane sortuje i
wyprowadza.
PoniewaŜ funkcje read i write oczekują argumentów typu char * i int, wskazana zmiennych całkowitych
(np. &tmp) poddano jawnej konwersji do typu char *.
72