Със следните 4, подробно коментирани програми, ще се запознаем с основните операции за запис и четене на файлове.
#include <iostream> #include <fstream> using namespace std; int main() { fstream f; // декларираме файлов поток f // Запис във файл f.open("a", ios::out); // свързваме го с физическия файл 'a' и го отваряме за запис f << "Аз съм файл.\n"; // записваме текст в него f.close(); // затваряме файла // Четене от файл string line; // тук ще поставяме прочетения ред f.open("a", ios::in); // свързваме f с физическия файл 'a' и го отваряме за четене while (getline (f, line)) { // опитваме се да прочетем един ред от файла, в line // ако не възникне грешка (примерно файлът е свършил) cout << line << endl; // отпечатваме прочетения ред } f.close(); // затваряме файла return 0; }
#include <iostream> #include <fstream> using namespace std; int main() { int i = 15, j = 7; fstream f; // декларираме файлов поток f // Запис в двоичен файл f.open("a", ios::binary | ios::out); // отваряме файла a, като двоичен за запис f.write((char*) &i, sizeof(i)); // записваме sizeof(i) на брой байта във файла, // започвайки от адреса на i f.write((char*) &j, sizeof(j)); // записваме sizeof(j) на брой байта във файла, // започвайки от адреса на j // функцията write приема като параметър указател от тип char* f.close(); // Четене от двоичен файл i = j = 0; f.open("a", ios::binary | ios::in); // отваряме файла a, като двоичен, за четене f.read((char*) &i, sizeof(i)); // четем sizeof(i) на брой байта в паметта на адрес: адресът на i f.read((char*) &j, sizeof(j)); // четем sizeof(j) на брой байта в паметта на адрес: адресът на j f.close(); // отпечатваме прочетеното cout << "i = " << i << endl; cout << "j = " << j << endl; }
#include <iostream> #include <fstream> using namespace std; int main() { fstream f; // декларираме файлов поток f string line; // Писане във файл, ползвайки fstream f.open("a", ios::out); // свързваме го с физическия файл 'a' и го отваряме за запис f << "Аз съм файл.\n"; // записваме текст в него if(!f.good()) { // проверяваме дали всичко е минало добре cout << "Възникна грешка!\n"; } f.close(); // затваряме файла // Писане във файл, ползвайки ofstream ofstream output("b"); // декларираме файлов поток за запис 'output' // конструкторът автоматично отваря файла 'b' за запис output << "Аз съм 2-ри файл."; // записваме текст в него output.close(); // затваряме файла // Четене от файл, ползвайки fstream f.open("a", ios::in); // свързваме 'f' с физическия файл 'a' и го отваряме за четене while (!f.eof()) { // докато не сме стигнали края на файла getline(f, line); // прочитаме един ред от него и го поставяме в 'line' cout << line << endl; // отпечатваме прочетения ред } f.close(); // затваряме файла // Четене от файл, ползвайки ifstream ifstream input("b"); // декларираме файлов поток за четене 'input' // конструкторът автоматично отваря файла 'b' за четене while (getline(input, line, '-')) { // докато успешно четем редове от него cout << line << endl; // ги отпечатваме // ако редът съдържа '-' го прочитаме само до '-' } input.close(); // затваряме файла // Използване на файлов указател за определяне размера на файл input.open("b"); input.seekg(0, ios::end); // позиционираме файловия указател в края на файла // първият параметър указва отместването, в случая - 0 // вторият параметър указва от къде и в каква посока // да започне отместването, в случая - от края към началото cout << "\nРазмерът на файла е "; cout << input.tellg() << " байта\n"; // отпечатваме текущата позиция на файловия указател input.close(); // Забележете, че в зависимост от използвания набор от символи (кодировка) при записа на файла // е възможно кирилските символи да се записват с 2 байта (UTF-8). return 0; }
#include <iostream> #include <fstream> #define N 10 using namespace std; int main() { // низ и масив, които ще запишем в двоичен файл string s = "Аз съм двоичен файл.\n"; int array[N] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // Запис в двоичен файл fstream f("a", ios::binary | ios::out); // отваряме файла 'a', като двоичен за запис f.write(s.data(), s.size()); // записваме s.size() на брой байта във файла, // започвайки от адрес s.data() // data() връща указател към първия символ от низа f.close(); // затваряме файла // Отново запис в двоичен файл f.open("a", ios::binary | ios::out | ios::app); // отваряме файла 'a', като двоичен, за добавяне f.write((char*)array, N * sizeof(int)); // записваме N*sizeof(int) на брой байта, започвайки от array // array е указател към първия елемент на масива f.close(); // заделяме памет, в която ще прочетем низа и масива int sizeOfString = s.size(); // запомняме, колко байта ще четем за низа char* pChar = new char[sizeOfString]; // тук ще четем низа int* pInt = new int[N]; // тук ще четем масива от цели числа // Четене от двоичен файл f.open("a", ios::binary | ios::in); // отваряме файла 'a', като двоичен, за четене if (f.is_open()) { // ако файлът е отворен успешно f.read(pChar, sizeOfString); // четем sizeOfString на брой байта в паметта, на адрес pChar f.read((char*)pInt, N * sizeof(int)); // четем N*sizeof(int) на брой байта на адрес pInt } else cout << "Файлът не може да се отвори!" << endl; f.close(); // отпечатваме прочетеното for(int i = 0; i < sizeOfString; i++) cout << pChar[i]; for(int i = 0; i < N; i++) cout << pInt[i] << ' '; cout << endl; // освобождаваме заделената памет delete[] pChar; delete[] pInt; }
Функцията-шаблон представлява обобщена функция, която може да се изпълнява за различни типове. Или казано иначе: тя представлява макет (шаблон) на функция, кодът, на която се уточнява за конкретен тип, при компилацията. Тя приема, като параметър тип.
Да разгледаме следните 2 функции за размяна на 2 променливи:
void swapInt(int &a, int &b) { int c = a; a = b; b = c; } void swapDouble(double &a, double &b) { double c = a; a = b; b = c; }
Те са почти еднакви с единствената разлика, че първата работи с int, а втората с double. Можем да ги обобщим в една функция-шаблон:
template <class T> void mySwap(T &a, T &b) { T c = a; a = b; b = c; }
Сега можем да използваме новата функция, за да разменяме променливи от всякакъв тип. Това е демонстрирано в следната програма:
#include <iostream> using namespace std; template <class T> void mySwap(T &a, T &b) { T c = a; a = b; b = c; } struct point { // структура с 2 полета int int x; int y; }; int main() { // разменяме 2 променливи от тип int int ia = 10, ib = 20; mySwap(ia, ib); cout << "ia = " << ia << ", ib = " << ib << "\n"; // разменяме 2 променливи от тип double double da = 3.4, db = 6.5; mySwap(da, db); cout << "da = " << da << ", db = " << db << "\n"; // разменяме 2 променливи от тип point point pointA, pointB; pointA.x = 13; pointA.y = 15; pointB.x = 45; pointB.y = 54; mySwap(pointA, pointB); cout << "pointA = (" << pointA.x << ", " << pointA.y << ")\n"; cout << "pointB = (" << pointB.x << ", " << pointB.y << ")\n"; return 0; }
Освен функции-шаблони има и класове-шаблони. Ще променим класа от файл objects_1.cpp да бъде клас-шаблон. Ще му добавим и метод, който връща стойност:
#include <iostream> using namespace std; template <class T> // с template <class T> пред името на класа указваме, че class Point { // той е шаблон. При компилация, T навсякъде в класа private: // ще бъде заменено със зададения от нас тип T x; T y; public: Point(T x, T y) { this->x = x; this->y = y; } void print() { cout << '(' << x << ", " << y << ")\n"; } T sum() { return x + y; } }; int main() { Point<char> a(35, 37); Point<int> b(30, 40); Point<double> c(50.8, 6.13); cout << "Обектът a: "; a.print(); cout << "Сумата от координатите на a е " << a.sum() << "\n\n"; cout << "Обектът b: "; b.print(); cout << "Сумата от координатите на b е " << b.sum() << "\n\n"; cout << "Обектът c: "; c.print(); cout << "Сумата от координатите на c е " << c.sum() << "\n\n"; return 0; }
Често се налага да съхраняваме данни в масив, без предварително да знаем броя им. Ако декларираме масив от 1000 елемента, а използваме само 20 ще бъде много неефективно. Вместо това можем да заделим памет, след като вече сме разбрали колко елемента ще съдържа масивът. При добавяне на нови елементи ще заделяме нова памет, ще копираме масива и ще освобождаваме старата. Ако обаче правим това при всяко добавяне ще бъде неефективно. Затова можем да заделяме по-голям блок от памет (примерно за 4 елемента) и чак, когато масивът се запълни да заделим нова. Това е реализирано в следващата програма. В нея използваме клас-шаблон, което позволява да съхраняваме в масива елементи от произволен тип.
#include <iostream> #include <cstdlib> // нужна за използване на exit() #define DEFAULT_BLOCK_SIZE 4 using namespace std; template <class T> class Array { private: unsigned size, realSize; // size е размера на масива, realSize е заделената памет T *a; // указател към началото на масива unsigned blockSize; // по колко памет ще се заделя, когато е нужно public: Array(); // празен конструктор Array(unsigned aSize, unsigned aBlockSize = DEFAULT_BLOCK_SIZE); // конструктор по зададен брой елементи и размер на блока // ако не се подаде размер се използва DEFAULT_BLOCK_SIZE ~Array(); // деструктор void add(T data); // добавяне на елемент void setBlockSize(unsigned aBlockSize); // задава по колко памет да се заделя T &operator[] (unsigned i); // оператор за достъп до елементите unsigned getSize() const; // връща броя на елементите // const забранява на метода да променя променливите на класа void print() const; // отпечатва масива на екрана }; int main() { Array<float> b; float temp; cout << "Въведете някакъв брой ненулеви числа, следвани от 0\n"; while(true) { cin >> temp; if(!temp) break; b.add(temp); } cout << "Въведохте следните " << b.getSize() << " числа:\n"; b.print(); Array<int> c(b.getSize()); c.setBlockSize(2); for(int i = 0; i < b.getSize(); i++) c[i] = b[i]; while(c.getSize() < 2) c.add(0); c[0] = 28; c[1] = 28; cout << "\nМасивът c:\n"; c.print(); return 0; } template <class T> Array<T>::Array() { size = 0; realSize = blockSize = DEFAULT_BLOCK_SIZE; // правим реалния размер колкото 1 блок a = new T[realSize]; // заделяме памет } template <class T> Array<T>::Array(unsigned aSize, unsigned aBlockSize) { if(aBlockSize < 1) { // ако е зададен невалиден размер blockSize = DEFAULT_BLOCK_SIZE; // се ползва стандартния cout << "Размерът на блока трябва да е >= 1! Ще се ползва стандартният: " << DEFAULT_BLOCK_SIZE << endl; } else blockSize = aBlockSize; size = aSize; realSize = blockSize * (size / blockSize + 1); // пресмятаме колко памет трябва да се задели a = new T[realSize]; // заделяме памет } template <class T> Array<T>::~Array() { delete[] a; // освобождаваме заделената памет } template <class T> void Array<T>::add(T data) { if(size == realSize) { // ако масивът вече е малък, заделяме повече памет T* old = a; // декларираме временен указател към масива a = new T[realSize += blockSize]; // увеличаваме физическия размер и заделяме памет за нов масив for(int i = 0; i < size; i ++) // копираме стария в новия масив a[i] = old[i]; delete[] old; // освобождаваме паметта за стария масив } a[size++] = data; // добавяме новия елемент и след това увеличаваме размера } template <class T> T &Array<T>::operator[] (unsigned i) { if(i < 0 || i >= size) { // ако се опитваме да достъпим несъществуващ елемент cerr<<"Няма елемент с индекс "<<i<<endl;// извеждаме съобщение за грешка exit(1); // излизаме от програмата със съобщение за грешка 1 } return a[i]; } template <class T> unsigned Array<T>::getSize() const { return size; } template <class T> void Array<T>::setBlockSize(unsigned aBlockSize) { if(aBlockSize < 1) { cerr << "Размерът на блока трябва да е >= 1!" << endl; return; } blockSize = aBlockSize; } template <class T> void Array<T>::print() const { for(int i = 0; i < size; i++) cout << a[i] << ' '; cout << endl; }
За тренировка, да се снабди класът Array с метод(и) за изтриване на елементи.
Да се промени класът, така че да не се заделя памет преди първото добавяне на елемент.
Да се добавят методи за запис и четене на масива от файл.