Bielecki Visual C 6.0 (Podstawy programowania), Jan Bielecki


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

Funkcje wejścia-wyjścia

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.

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

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ń. Biegłe programowanie w C++ wymaga starannego zapoznania się z podanymi tam opisami.

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.

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

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)

{

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.

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ć

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--;

}

Instrukcja iteracyjna

Instrukcja iteracyjna ma postać

while(c)

s

w której c jest wyrażeniem orzecznikowym, a s jest pojedynczą instrukcją.

Wykonanie instrukcji iteracyjnej 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--;

}

Należy zauważyć, że instrukcję iteracyjną o postaci

d

while(c) {

s s ... s

e;

}

w której d jest instrukcją deklaracyjną, a e jest wyrażeniem, można przedstawić w postaci

for(d c ; e) {

s s ... s

}

Taka odmiana instrukcji iteracyjnej dobrze nadaje się do opisania 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;

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;

}

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 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.

-2.4 2.e4 .2 // nazwy zmiennych typu double

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.

#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 1-bajtowe, zmienne typu int 2-bajtowe, a zmienne typu double 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;

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

###

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

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

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.

#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;

}

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;

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.

struct Number {

bool isFixed;

union {

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.

Stan strumienia

Początkowo strumień znajduje się w stanie dobrym, ale na skutek błędu operacji wejścia-wyjścia albo próby wprowadzenia nie istnieją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.

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.

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;

}

Program zlicza dane liczbowe wprowadzone z klawiatury.

Ś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.

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.

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

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.

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).

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

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

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).

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>

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.

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.

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 ";

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;

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.

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;

}

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;

}

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.

#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;

}

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

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);

}

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;

}

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.

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;

}

Użyto operatora zakresu (::). Dzięki niemu, napis ::p jest nazwą zmiennej globalnej.

Wywołania rekurencyjne

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;

}

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;

return 0;

}

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++;

}

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. Jest niszczona po jawnym wykonaniu operacji delete.

Uwaga: Obszar, w którym są tworzone zmienne kontrolowane jest nazywany stertą. Sterta jest obszarem pamięci, do którego można dokładać zmienne, a następnie usuwać je w dowolnej kolejności.

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.

Uwaga: Wyrażenie określające liczbę elementów tablicy nie musi być wyrażeniem stałym.

Wykonanie operacji

delete [] ptr

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ą.

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.

#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

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();

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;

}

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;

}

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);

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;

}

}

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).

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)

(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

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.

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.

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.

Dodatek D

Funkcje 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()

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!).

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;

}

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.

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);

}

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 *.



Wyszukiwarka

Podobne podstrony:
Visual C 6 0 Podstawy programowania Jan Bielecki(1)
Jan Bielecki Visual C 6 podstawy programowania
Jan Bielecki Visual C 6 0 Podstawy programowania
Visual C 6 0 Podstawy programowania(1)
Visual C 6 0 Podstawy Programowania [73 strony]
Nowa podstawa programowa WF (1)
1 Podstawy programowania dialogowego
nowa podstawa programowa sp
11-nkb~1, wisisz, wydzial informatyki, studia zaoczne inzynierskie, podstawy programowania, l2
2-eukl~1, wisisz, wydzial informatyki, studia zaoczne inzynierskie, podstawy programowania, l2
Zmiany w podstawie programowej w zakresie edukcji matematycznej, Wczesna edukacja, Materiały do prac
1-algo~1, wisisz, wydzial informatyki, studia zaoczne inzynierskie, podstawy programowania, l2
c-zadania-w3, wisisz, wydzial informatyki, studia zaoczne inzynierskie, podstawy programowania, kol
Wychowanie w nowej podstawie programowej katechezy, szkoła, Rady Pedagogiczne, wychowanie, profilakt
PP temat6, Podstawy programowania
PODSTAWA PROGRAMOWA WYCHOWANIA PRZEDSZKOLNEGO
Laboratorium Podstaw Programowania 2

więcej podobnych podstron