графите са нелинейни структури от данни; делят се на два вида:
-
неориентиран граф – разглежда се като G = < A, P >, където A е множество от върхове и P е множество от ненаредени двойки върхове, наречени ребра;
-
ориентиран граф – разглежда се като D = < A, R >, където A е множество от върхове и R е множество от наредени двойки върхове, наречени дъги;
ще разгледаме по-подробно крайните ориентирани графи, т.е. графите D = < A, R > при които A е крайно множество;
всъщност теориите на ориентираните и неориентираните графи са независими една от друга и разглеждат различни обекти;
нека е даден ориентиран граф D = < A, R >;
нека A1 A, R1 R; графът D1 = < A1, R1 > се нарича част от D;
ако A1 = A, казваме че D1 е субграф на D;
нека A2 A, R2 R, така че R2 съдържа всички дъги с краища върхове от A2 и само тях; графът D2 = < A2, R2 > се нарича подграф на D; ако D2 D, казваме че D2 е истински подграф на D;
отмерен ориентиран граф е такъв, при който всички дъги имат присвоена величина, която наричаме тегло;
пример:
нека графът D се състои от всички кръстовища и свързващите ги улици в даден град;
ако D1 съдържа всички кръстовища и всички улици, които ги свързват в даден квартал на града, то D1 е подграф на D;
ако D2 съдържа всички кръстовища в града и свързващите ги асфалтирани улици, то D2 е субграф на D;
ако D3 съдържа всички кръстовища и улици по които се движи градския транспорт, то D2 е част от D;
ако (a, b) R и a = b, дъгата (a, b) наричаме примка;
нека X A и (a, b) R;
ако a X, b X, казваме че дъгата (a, b) влиза в X;
ако a X, b X, казваме че дъгата (a, b) излиза от X;
дъгите, които влизат в X и излизат от X се наричат инцидентни с X;
ще означаваме с RX+ R множеството от всички дъги, които излизат от X; броят на елементите на RX+ се нарича полустепен на изхода
на X, означаваме od (X);
ще означаваме с RX- R множеството от всички дъги, които излизат от X; броят на елементите на RX- се нарича полустепен на входа
на X, означаваме id (X);
с td (X) означаваме броят на дъгите, инцидентни с X, т.е.
td (X) = id (X) + od (X);
пример:
5
1
3
4
2
това е изображение на следния граф:
D = < { 1, 2, 3, 4, 5}, { (1, 2), (2, 1), (1, 1), (2, 3), (4, 3) } >;
нека X = { 2, 4};
имаме RX+ = { (2, 3), (4, 3), (2, 1) } и od (X) = 3,
RX- = { (1, 2)} и id (X) = 1, td (X) = id (X) + od (X) = 3 + 1 = 4;
лесно се вижда, че сумата на полустепените на входа и на изхода на всички върхове на графа D дава броят на всички дъги в D, които не са примки;
последователността (a1, a2), (a2, a3), …, (an-1, an) от дъги наричаме път от a1 до an; ако a1 = an, казваме че пътят е цикъл;
ако ai aj при i j, казваме че пътят е прост;
ако върховете a1, …, an-1 са два по два различни и a1 = an, казваме че пътят е прост цикъл;
прост път, който съдържа всички върхове на графа се нарича Хамилтонов път;
прост цикъл, който съдържа всички върхове на графа се нарича Хамилтонов цикъл;
например в горния граф (1, 2), (2, 3) е прост път,
(1, 2), (2, 1) е прост цикъл; в него не съществуват Хамилтонови пътища и цикли;
граф, който съдържа поне един цикъл се нарича цикличен,
ако не съдържа нито един цикъл, казваме че графът е ацикличен;
върхът b е достижим от върха a, ако съществува път от a до b;
върхът a наричаме изолиран връх, ако той не е достижим от никой връх и никой връх не е достижим от него; в горния граф върхът 5 е изолиран връх, 3 е достижим от 1, 4 не е достижим от 3;
нека J A; J наричаме минимално пораждащо подмножество
на A, ако:
-
всеки връх на A е достижим от някой връх от J;
-
J е минимално по включване с това свойство;
например минималното пораждащо подмножество на ацикличен ориентиран граф се определя като множеството от всички върхове с полустепен на входа 0;
ориентиран граф D = < A, R > с върхове a1, a2, …, an може еднозначно да бъде определен с квадратна матрица от ред n; тази матрица се нарича матрица на съседство; ще я означаваме с X; тя се дефинира по следния начин X = (xij), xij = 1, ако (ai, aj) R и xij = 0, ако (ai, aj) R;
за отмерен граф се използва матрица на теглата W = (wij),
wij = теглото на дъгата (ai, aj), ако (ai, aj) R и wij = 0, ако (ai, aj) R;
за графа D дефинираме матрица на пътищата P по следния начин: P = (pij), pij = 1, ако има път от ai до aj и pij = 0, ако няма път от ai до aj;
ще отбележим, че матрицата P не определя еднозначно графа;
Теорема: Нека Y = Xh, h N; тогава yij дава броя на различните пътища с дължина h от ai до aj;
Следствие: Ако Xh = O за някое h, то графът D е ацикличен;
ориентираният граф D = < A, R > може да се представи и чрез списъци по следния начин: списък от върховете и за всеки връх списък от дъгите с начало в този връх; първият списък може да се замени с масив;
основни операции с графи:
-
създаване на граф;
-
проверка за празен граф;
-
търсене на връх или дъга в граф;
-
добавяне на връх или дъга;
-
премахване на връх или дъга;
-
намиране на път между два върха с точно определена дължина, в частност с минимална или максимална дължина;
-
намиране на циклите, в които участва даден връх на графа;
-
обхождане на граф; под обхождане на граф разбираме посещаване на всеки връх точно веднъж и евентуално извършване на допълнителни действия свързани с конкретната задача; съществуват два основни вида обхождане:
-
обхождане в дълбочина – ако обхождаме връх u, то непосредствено след това обхождаме един от наследниците му v и останалите наследници на u се обхождат когато се обходят всички наследници на v;
-
обхождане в широчина – ако обхождаме връх v, то непосредствено след това обхождаме всичките му наследници и тогава преминаваме към техните наследници;
27. Дървета.
ацикличен ориентиран граф, в който само един връх, наречен корен има полустепен на входа 0 и всички останали върхове имат полустепен на входа 1 се нарича ориентирано дърво; върховете с полустепен на изхода 0 се наричат листа на дървото; дължината на пътя, измерена в брой дъги, от корена до даден връх се нарича ниво на този връх – този път при дърветата е единствен, тъй като всеки връх има единствен предшественик;
два върха наричаме братя, ако са наследници на един и същи връх;
пример:
1
2
3
4
5
6
7
8
в това дърво 1 е корен, на първо ниво са 2, 3, 4, на второ ниво са
5, 6, 7 и на трето ниво е 8; листата са 2, 6, 7, 8; 2, 3, 4 са братя, 6 и 7 не са братя, въпреки че са на едно ниво;
всеки вътрешен връх в едно дърво (който не корен и не е листо) може да се разглежда като корен на дърво, което се състои от неговите наследници, от наследниците на техните наследници и т.н.;
например за горното дърво върхът 3 е корен на поддърво, което съдържа върховете 3, 5, 6, 8;
този факт дава основание да се даде алтернативна рекурсивна дефиниция за понятието дърво:
дървото е крайно множество T от върхове, такива че:
-
съществува точно един избран връх, наречен корен;
-
всички останали върхове се съдържат в k 0 на брой две по две непресичащи се подмножества T1, T2, …, Tk, които се наричат поддървета на корена на T; корените на тези поддървета наричаме наследници на корена на T;
ако в дървото е въведена наредба на върховете по всяко ниво, дървото се нарича наредено;
дърво в което всеки връх има полустепен на изхода 0, 1 или 2 се нарича двоично (бинарно) дърво;
ще дадем аналогична на горната рекурсивна дефиниция за двоично дърво: двоично дърво е крайно множество от върхове, което е или празно, или се състои от един връх, наречен корен и две непресичащи се двоични дървета, които се наричат ляво и дясно поддърво на корена на дървото;
произволно дърво може да се представи чрез двоично по следния рекурсивен алгоритъм:
-
коренът на двоичното дърво е коренът на изходното дърво;
-
ляво поддърво на корена на двоичното дърво е преобразуваното най-ляво поддърво на корена на изходното дърво;
-
дясно поддърво на корена на двоичното дърво е преобразуваното дърво на десния брат на корена;
ще използваме рекурсивната дефиниция за двоично дърво, за да опишем структура за елемент на дърво;
template < class KeyType >
struct btree {
KeyType key;
btree < KeyType> *left, *right;
} ;
по горния алгоритъм елемент на произволно дърво може да се опише по следния начин:
template < class KeyType >
struct tree {
KeyType key;
tree < KeyType > *first_left, *next_right;
} ;
рекурсивната дефиниция на двоично дърво предполага лесно използване на рекурсивни функции за операции с двоични дървета;
пример е обхождането на дърво, за което съществуват три основни метода:
-
смесен ред на обхождане:
-
обхожда се лявото поддърво;
-
посещава се корена, т.е. извършват се действия свързани с конкретната задача;
-
обхожда се дясното поддърво;
-
низходящ ред на обхождане:
-
посещава се корена;
-
обхожда се лявото поддърво;
-
обхожда се дясното поддърво;
-
възходящ ред на обхождане:
-
обхожда се лявото поддърво;
-
обхожда се дясното поддърво;
-
посещава се корена;
ще реализираме три функции, които реализират трите вида обхождане; функцията p (btree < KeyType > *t) реализира посещаване на корена на дървото, сочено от t;
функция, която реализира смесен ред на обхождане на двоично дърво;
template < class KeyType >
void inorder (btree *t)
{ if (t != NULL)
{ inorder (t -> left);
p (t);
inorder (t -> right);
}
}
функция, която реализира низходящ ред на обхождане на двоично дърво;
template < class KeyType >
void preorder (btree *t)
{ if (t != NULL)
{ p (t);
inorder (t -> left);
inorder (t -> right);
}
}
функция, която реализира възходящ ред на обхождане на двоично дърво;
template < class KeyType >
void postorder (btree *t)
{ if (t != NULL)
{ inorder (t -> left);
inorder (t -> right);
p (t);
}
}
аритметичните изрази могат да се представят по следния начин:
-
операциите са в нелиста;
-
операндите са в листа;
-
ако една операция се съдържа във връх v, то v има толкова наследника, колкото са аргументите на операцията и тя се прилага върху изразите, които отговарят на поддърветата на v;
например изразът a – b + c*d се представя по следния начин:
+
–
*
d
c
b
a
нека p (t) извежда ключа в съответния връх на дървото;
-
при смесено обхождане получаваме изразът във възприетия инфиксен запис: a – b + c*d;
-
при низходящо обхождане получаваме изразът в така наречения префиксен запис: + - a b * c d;
-
при възходящо обхождане получаваме изразът в постфиксен запис или още обратен полски запис;
обратният полски запис се използва от компилаторите за пресмятане на изрази; ще опишем алгоритъм за пресмятане на израз, който е в обратен полски запис: (разполагаме със стек, който в началото е празен)
-
i = 1;
-
прочитаме i-тия символ от израза;
ако той е операнд, го включваме в стека;
ако той е операция, изключваме двата операнда във върха на стека, прилагаме за тях операцията и полученото го включваме в стека; i = i + 1; ако има непрочетени символи,
премини към 2., в противен случай премини към 3.;
-
резултатът е във върха на стека; край;
основни операции с дърво:
-
създаване на дърво;
-
проверка за празно дърво;
-
достъп до корена на дървото;
-
намиране на броя на поддърветата на даден връх;
-
включване на поддърво в дадено дърво;
-
изключване на поддърво от дърво;
-
унищожаване на дърво;
-
обхождане на дърво;
-
включване на връх в двоично дърво за търсене;
-
изключване на връх от двоично дърво за търсене;
28. Създаване на двоично дърво и включване на връх в в двоично дърво за търсене.
ще използваме следната структура за елемент на дървото:
struct btree {
int key;
btree *left, *right;
} ;
функция за създаване:
void create (btree *&t)
{ int x; char ch;
cout << “Ключ на корен: “;
cin >> x;
if ( (t = new btree) == NULL)
{ cout << “Няма достатъчно памет!\n” << endl;
exit (1);
}
t -> left = t -> right = NULL; t -> key = x;
cout << “Ляво поддърво с ключ на корена “ << x
<< “ Y/N? “;
ch = cin.get ();
if ( (ch = cin.get ()) == ‘Y’ || ch == ‘y’ )
create (t -> left);
cout << “Дясно поддърво с ключ на корена “ << x
<< “ Y/N? “;
ch = cin.get ();
if ( (ch = cin.get ()) == ‘Y’ || ch == ‘y’ )
create (t -> right);
}
за реализиране на конкретно дърво може да се използва следната главна функция:
void main ()
{ btree *tree;
create (tree);
…
}
под двоично дърво за търсене разбираме двоично дърво за всеки връх на което стойностите на всички ключове в лявото поддърво са по-малки от стойността на ключа във върха x и стойностите на всички ключове в дясното поддърво са не по-малки от стойността на ключа във върха x; такова дърво още наричаме двоично дърво за търсене с повторения, тъй като в него се допускат върхове с еднакви ключове; ако изискването за стойностите на ключовете на върховете в дясното поддърво на върха x е да са по-големи от стойността на ключа във върха x, тогава двоичното дърво за търсене е без повторения;
ще опишем функция за добавяне на връх в двоично дърво за търсене с повторения;
template < class KeyType >
struct btree {
KeyType key;
btree < KeyType > *left, *right;
} ;
template < class KeyType >
void insert ( KeyType x, btree < KeyType > *t)
{ if (t == NULL)
{ if ( ( t = new btree < KeyType >) == NULL)
{ cout << “Няма достатъчно памет!” << endl;
exit (1);
}
t -> left = t -> right = NULL; t -> key = x;
return;
}
if ( x < t -> key ) insert (x, t -> left);
else insert (x, t -> right);
}
за реализиране на конкретно дърво за търсене с повторения може да използваме следната главна функция:
void main ()
{ btree *tree = NULL;
insert (5, tree);
insert (3, tree);
insert (4, tree);
…
}
ще отбележим, че смесеното обхождане на дърво за търсене (с или без повторение) посещава върховете, сортирани във възходящ ред;
29. Обектно-ориентирана реализация на двоично дърво за търсене.
ще опишем клас за двоично дърво за търсене без повторения;
първо ще опишем клас за елемент от дървото;
файл treenode.h
#ifndef TREENODE_H
#define TREENODE_H
template < class NodeType > class Tree;
template < class NodeType >
class TreeNode {
friend class Tree < NodeType >;
public:
TreeNode (const NodeType &d) :
leftPtr (0), rightPtr (0), data (d) { }
NodeType getData () const
{ return data; }
private:
NodeType data;
TreeNode < NodeType > *leftPtr, *rightPtr;
} ;
#endif
сега ще опишем клас за самото дърво; той ще съдържа три функции за обхождане – в смесен, низходящ и възходящ ред; при обхождането стойностите на ключовете във върховете се извежат на екрана;
двоичното търсещо дърво, което реализираме може да се използва за сортиране на данни – достатъчно е да се добавят всички елементи и след това да се извърши смесено обхождане;
файл tree.h
#ifndef TREE_H
#define TREE_H
#include
#include “treenode.h”
template < class NodeType >
class Tree {
public:
Tree ();
void insertNode (const NodeType &);
void preOrderTraversal () const;
void inOrderTraversal () const;
void postOrderTraversal () const;
private:
TreeNode < NodeType > *rootPtr;
void insertNodeHelper (TreeNode *&, const NodeType &);
void preOrderHelper (TreeNode *) const;
void inOrderHelper (TreeNode *) const;
void postOrderHelper (TreeNode *) const;
} ;
template < class NodeType >
Tree < NodeType > :: Tree ()
{ rootPtr = 0; }
template < class NodeType >
void Tree < NodeType > :: insertNode ( const NodeType &value )
{ insertNodeHelper (rootPtr, value); }
template < class NodeType >
void Tree < NodeType > :: insertNodeHelper
( TreeNode < NodeType > *& ptr, const NodeType &value )
{ if (ptr == NULL)
{ ptr = new TreeNode (value);
assert (ptr != NULL);
}
else if (value < ptr -> data)
insertNodeHelper ( ptr -> leftPtr, value );
else if (value > ptr -> data)
insertNodeHelper ( ptr -> rightPtr, value );
else cout << value << “ е дубликат!” << endl;
}
template < class NodeType >
void Tree < NodeType > :: preOrderTraversal () const
{ preOrderHelper (rootPtr); }
template < class NodeType >
void Tree < NodeType > :: preOrderHelper
(TreeNode *ptr) const
{ if (ptr != 0)
{ cout << ptr -> data << ‘ ‘;
preOrderHelper (ptr -> leftPtr);
preOrderHelper (ptr -> rightPtr);
}
}
template < class NodeType >
void Tree < NodeType > :: inOrderTraversal () const
{ inOrderHelper (rootPtr); }
template < class NodeType >
void Tree < NodeType > :: inOrderHelper
(TreeNode *ptr) const
{ if (ptr != 0)
{ inOrderHelper (ptr -> leftPtr);
cout << ptr -> data << ‘ ‘;
inOrderHelper (ptr -> rightPtr);
}
}
template < class NodeType >
void Tree < NodeType > :: postOrderTraversal () const
{ postOrderHelper (rootPtr); }
template < class NodeType >
void Tree < NodeType > :: postOrderHelper
(TreeNode *ptr) const
{ if (ptr != 0)
{ postOrderHelper (ptr -> leftPtr);
postOrderHelper (ptr -> rightPtr);
cout << ptr -> data << ‘ ‘;
}
}
#endif
ще опишем програма, която използва гореописаните класове;
файл treetest.cpp
#include
#include
#include “tree.h”
using std::cout;
using std::cin;
using std::endl;
using std::setiosflags;
using std::ios;
using std::setprecision;
void main ()
{ Tree < int > intTree;
int intValue, i;
cout << “Въведете 10 int – стойности:\n”;
for (i = 0; i < 10; i++)
{ cin >> intValue;
intTree.insertNode (intValue);
}
cout << “Низходящо обхождане:\n“;
intTree.preOrderTraversal ();
cout << endl;
cout << “Смесено обхождане:\n“;
intTree.inOrderTraversal ();
cout << endl;
cout << “Възходящо обхождане:\n“;
intTree.postOrderTraversal ();
cout << endl;
Tree < double > doubleTree;
double doubleValue;
cout << “Въведете 10 double стойности:\n”
<< setiosflags (ios::fixed | ios::showpoint)
<< setprecision (1);
for (i = 0; i < 10; i++)
{ cin >> doubleValue;
doubleTree.insertNode (doubleValue);
}
cout << “Низходящо обхождане:\n“;
doubleTree.preOrderTraversal ();
cout << endl;
cout << “Смесено обхождане:\n“;
doubleTree.inOrderTraversal ();
cout << endl;
cout << “Възходящо обхождане:\n“;
doubleTree.postOrderTraversal ();
cout << endl;
}
ще отбележим, че конкретният вид на двоичното дърво за търсене съществено зависи от реда по който се подават данните;
търсенето на елемент в дървото в средния случай се осъществява с
log2n сравнения; това означава, че в дърво с 1000 върха съществуването на елемент с даден ключ може да се провери средно с около 10 сравнения, при дърво с 1000000 върха този брой е 20;
съществува, обаче, най-лош случай, в който дървото се изражда в списък – когато елементите се подават в сортиран ред; тогава сложността за търсене е линейна, т.е. от порядъка на броя на върховете на дървото;
30. Обработка на файлове с последователен достъп.
в C++ всеки файл се разглежда като последователност от n байта с номера от 0 до n-1, като всеки файл завършва със символа EOF;
за да се извърши вход и изход с файлове в C++ в програмата трябва да се включат заглавните файлове ifstream, ofstream; заглавният файл fstream включва и двата файла и някои други допълнителни функции; връзката между файловете и програмата се осъществява от потоците (обекти от посочените класове);
файловете се отварят чрез създаване на обекти от класовете ifstream, ofstream, fstream или чрез обръщение към вече създадени обекти;
за всеки файл, който предстои да бъде отворен, се задава режим за отваряне, като някои от възможностите са следните:
-
ios::app - за присъединяване на данни в края на файла без да се променят данните, които се намират вече в него;
-
ios::in – отваряне на файл за въвеждане;
-
ios::out – отваряне на файл за извеждане;
-
ios::binary – отваряне на файл за двоичен вход или изход; използва се с другите режими чрез използване на операция побитово или (‘|’);
отварянето на файл за извеждане става чрез създаване на обект от класа ofstream, като при създаването се предават два параметъра на съответния конструктор на класа – името на файла и режима за отваряне;
за обект от класа ofstream режима за отваряне може да бъде ios::out или ios::app; в режима ios::out съществуващите файлове се унищожават и създават наново, а несъществуващите файлове се създават; аргументът режим за отваряне по премълчаване е ios::out; обект от класа ofstream може да бъде създаден без да се отваря файл – за целта не се подават аргументи на конструктора; чрез този обект по-късно може да бъде отворен файл чрез функцията елемент open, която има същите параметри като конструктора за отваряне;
операцията отрицание (‘!’) е предефинирана в класа ios и се използва върху обекти от този клас; операцията връща ненулева стойност (true) точно когато при отварянето на файла с помощта на обекта са се установили флаговете failbit или badbit; някои възможни причини за това са отваряне на несъществуващ файл за четене, опит за създаване на файл когато на диска няма свободно място и т.н.;
операцията за преобразуване към тип void * също е предефинирана в класа ios; тя преобразува обект от клас ios към указател, който получава стойност 0, ако за обекта са установени флаговете failbit или badbit; например в условието на цикъла while (cin >> c) { …} , обектът cin автоматично се преобразува към void *; това условие е истина докато за cin не е установен някой от флаговете failbit или badbit; въвеждането на признак за край на файла (в DOS и в Windows с клавишна комбинация Ctrl+Z) води до установяване на failbit за cin; в този случай условието не е истина и цикълът ще приключи;
при излизане от областта на действие на обект, свързан с файл автоматично се извиква неговия деструктор който освен че унищожава обекта, затваря файла свързан с него (ако има такъв отворен файл); затварянето на файл може да става и явно чрез функцията елемент close без параметри; препоръчва се явно затваряне, тъй като това води до икономия на ресурси и до
по-голяма яснота в програмата;
ще разгледаме програма за създаване на файл с последователен достъп, който съдържа записи съставени от номер на сметка, баланс и име на клиента; предполага се, че файлът се създава и обработва в сортиран вид;
#include
using std::cout;
using std::cin;
using std::ios;
using std::cerr;
using std::endl;
#include
using std::ofstream;
#include
void main ()
{ ofstream outclientFile ("clients.dat", ios::out);
if (!outclientFile)
{ cerr << "Файлът не може да бъде отворен!" << endl;
exit (1);
}
cout << "Въведете записи от номер " <<
"на сметката, име и баланс; Ctrl+Z за край:\n? ";
int account; char name[30]; double balance;
while (cin >> account >> name >> balance)
{
outclientFile << account << ' ' << name << ' ' << balance << endl;
cout << "? ";
}
outclientFile.close ();
}
31. Четене на данни от файл с последователен достъп.
отварянето на файл за въвеждане става чрез създаване на обект от класа ifstream, като при създаването се предават два параметъра на съответния конструктор на класа – името на файла и режима за отваряне; по премълчаване режимът за отваряне е ios::in, т.е. режим за въвеждане; подобно на случая с обекти от класа ofstream, обекти от класа ifstream могат да бъдат създавани без да се отваря файл едновременно с това; по-късно в програмата чрез тези обекти
могат да се отварят файлове с помощта на функцията елемент open;
обекти от класа ifstream могат да се използват като условие в цикъл, например: while (обект_от_клас_ifstream >> c) { …} ; при компилиране на условието се използва операцията за преобразуване на обект от клас ios към указател от тип void *, който приема нулева стойност, ако за обекта са установени флагове failbit или badbit; достигането до символа за край на файла включва флага failbit, така че цикълът ще приключи, ако се достигне до края на файла;
при последователно търсене на данни във файл често се налага последователна обработка на файла, като на всяка стъпка се започва от началото на файла; за целта всеки обект от класа istream има така наречения указател get на позицията за въвеждане от файла и всеки обект от класа ostream има указател put на позицията за извеждане във файла; стойността на тези указатели е поредният номер на байт във файла; класовете istream и ostream съдържат функции елементи за задаване на стойност на указателя на позицията във файла;
тези функции са следните:
-
seekg – за позициониране при въвеждане от файл
(от класа istream);
-
seekp – за позициониране при извеждане във файл
(от класа ostream);
всяка от тези функции има по два аргумента; първият аргумент е от тип long int и задава новата стойност за указателя на позицията във файла, а вторият аргумент може да приема една от следните стойности:
-
ios::beg – позициониране относно началото на файла;
-
ios::end – позициониране относно края на файла;
-
ios::cur – позициониране относно текущата позиция;
по премълчаване стойността на този аргумент е ios::beg;
примери:
обект_от_клас_istream.seekg (0) задава стойност 0 на указателя на позицията във файла, който е свързан с обекта, т.е. това води до позициониране в началото на файла;
обект_от_клас_istream.seekg (n) води до позициониране на байт с номер n;
обект_от_клас_istream.seekg (n, ios::cur) води до позициониране с n байта напред, т.е. спрямо текущата позиция;
обект_от_клас_istream.seekg (n, ios::end) води до позициониране с n байта преди края на файла;
обект_от_клас_istream.seekg (0, ios::end) води до позициониране в края на файла;
аналогични примери могат да се направят с обект от клас ostream и функцията елемент seekp;
функциите елемент tellg от класа istream и tellp от класа ostream връщат текущите стойности на указателите get и put; те връщат стойност от тип long int;
текстовите файлове с последователен достъп съдържат записи с неограничена дължина, за това обновяване на място не може да се извършва, т.е. възможно е единствено презаписване на целия файл или присъединяване на данни в края на файла;
примерна програма:
#include
using std::cin;
using std::cout;
using std::endl;
using std::cerr;
using std::ios;
#include
using std::ifstream;
#include
#include
using std::setiosflags;
using std::setw;
using std::setprecision;
void main ()
{ ifstream inclientFile (“clients.dat”, ios::in);
if (!inclientFile)
{ cerr << “Файлът не може да бъде отворен!” << endl;
exit (1);
}
int account;
char name[30];
double balance;
cout << setiosflags (ios::left) << setw (10) << “Сметка” << setw (30)
<< “Име” << “Баланс” << endl;
while (inclientFile >> account >> name >> balance)
cout << setiosflags (ios::left) << setw (10) << account << setw (30)
<< name << setprecision (2) << balance << endl;
}
32. Обработка на файлове с пряк достъп.
основно средство при обработване на файлове с пряк достъп е използването на записи с фиксирана дължина; в този случай, програмата може бързо да определи точното местоположение на даден запис спрямо началото на файла;
при обработка на файлове с пряк достъп записването, обновяването и изключването на данни може да стават без да се разрушават другите данни или да се презаписва целия файл;
функцията елемент write от класа ostream извежда фиксиран брой байтове в зададен поток, започвайки от дадено място в оперативната памет; когато потокът е свързан с файл, данните се извеждат започвайки от позицията, определена с помощта на указателя put;
първият аргумент на write е от тип const char * и определя мястото в оперативната памет, откъдето ще се извежда; вторият аргумент е от цял тип и определя броя на байтовете за извеждане;
функцията елемент read от класа istream въвежда фиксиран брой байтове от зададен поток в област от оперативната памет;
ако потокът е свързан с файл, байтовете се въвеждат започвайки от позицията, определена с помощта на указателя get;
първият аргумент на read е от тип char * и определя мястото в оперативната памет, където ще се въвеждат байтовете; вторият аргумент е от цял тип и определя броя на байтовете за въвеждане;
ако се планира програма, която чете неформатирани данни, записани с write, то е необходимо тя да бъде компилирана и изпълнена в операционна система, съвместима с програмата, която е записала данните;
ще опишем програма за обработка на сметки, която може да съхрани до 100 записа с фиксирана дължина за компания с до 100 клиенти;
програмата ще обновява сметки, ще унищожава сметки, ще вмъква нови сметки и ще извежда списък на всички сметки във форматиран текстов файл;
ще опишем заглавен файл, който определя типа на структурата;
файл clientdata.h
#ifndef CLIENTDATA_H
#define CLIENTDATA_H
struct clientData {
int account;
char firstName[15], lastName[15];
double balance;
} ;
#endif
ще опишем програма за инициализиране на файла със записите;
файл clientinit.cpp
#include
using std::cerr;
using std::endl;
using std::ios;
#include
using std::ofstream;
#include
#include “clientdata.h”
void main ()
{ ofstream outCredit ( “credit.dat”, ios::binary);
if (!outCredit)
{ cerr << “Файлът не може да бъде отворен!\n”;
exit (1);
}
clientData blankClient = { 0, “”, “”, 0.0 };
for (int i = 0; i < 100; i++)
outCredit.write (reinterpret_cast < const char *> (&blankClient),
sizeof (clientData));
}
всеки незапълнен запис от типа на структурата съдържа номер 0, празни низове за име и фамилия и баланс 0; във файла първоначално се задават 100 незапълнени записаи по-нататък програмата лесно ще може да определи дали някой от записите е запълнен или не;
в извикването на функцията елемент write се извършва преобразуване с операцията reinterpret_cast, тъй като аргументът
(&blankClient) е от тип clientData* и така извикването се компилира без синтактична грешка;
ще опишем програма за директно записване във файла с пряк достъп;
файл clientadd.cpp
#include
using std::cerr;
using std::cin;
using std::cout;
using std::endl;
using std::ios;
#include
using std::ofstream;
#include
#include “clientdata.h”
void main ()
{ ofstream outCredit ( “credit.dat”, ios::binary);
if (!outCredit)
{ cerr << “Файлът не може да бъде отворен!\n”;
exit (1);
}
cout << “Въведете номер на сметка от 1 до 100, 0 за край: “;
clientData client;
cin >> client.account;
while (client.account > 0 && client.account <= 100)
{ cout << “Въведете името и баланса: “;
cin >> client.firstName >> client.lastName >> client.balance;
outCredit.seekp ( (client.account-1)*sizeof (clientData));
outCredit.write (reinterpret_cast < const char *> (&client),
sizeof (clientData));
cout << “Въведете номер на сметка от 1 до 100, 0 за край: “;
cin >> client.account;
}
}
програмата записва данни във файла чрез комбинация от функциите seekp и write – функцията seekp установява указателя put на зададена позиция във файла и след това данните се извеждат с write;
от номера на сметката се изважда 1, тъй като номерацията на байтовете във файла започва от 0;
накрая ще опишем програма, която извежда непразните записи;
файл clientprint.cpp
#include
using std::cout;
using std::endl;
using std::ios;
using std::cerr;
#include
using std::setprecision;
using std::setiosflags;
using std::setw;
#include
using std::ifstream;
using std::ostream;
#include
#include “clientdata.h”
void outputLine (ostream &, const clientData &);
void main ()
{ ifstream inCredit (“credit.dat”, ios::in);
if (!inCredit)
{ cerr << “Файлът не може да бъде отворен!”;
exit (1);
}
cout << setw (10) << “Сметка:“
<< setw (16) << “Име:“ << setw (16) << “Фамилия:”
<< setw (10) << “Баланс:” << endl;
clientData client;
inCredit.read (reinterpret_cast < char * > (&client), sizeof (clientData));
while (inCredit && !inCredit.eof ())
{ if (client.account != 0)
outputLine (cout, client);
inCredit.read (reinterpret_cast (&client), sizeof (clientData));
}
}
void outputLine (ostream &output, const clientData &c)
{ output << setw (10) << c.account
<< setw (16) << c.firstName << setw (16) << c.lastName
<< setprecision (2) << setiosflags (ios::fixed | ios::showpoint)
<< setw (10) << c.balance << endl;
}
програмата последователно чете записи от файла и извежда тези от тях, които са непразни; четенето с функцията read приключва, ако се достигне края на файла (тогава !inCredit.eof () става лъжа) или ако възникне грешка по време на четене (тогава inCredit става лъжа);
записите се извеждат във възходящ ред по номера на сметката – това е следствие от съхраняването на записите във файл с пряк достъп;
обработката на файлове с пряк достъп е по-бърза, но изисква повече памет, тъй като се налага да се съхраняват незапълнени записи;
33. Макроопределения. Включване на файлове.
Сподели с приятели: |