ТОП 10:

Массивы структур и бинарные файлы



Структуры

 

В языке С++ структура является видом класса и обладает всеми его свойствами. Как правило, описание структуры определяет новый (абстрактный) тип данных, имя которого можно использовать в дальнейшем наряду со стандартными типами. Структуры предназначены для объединения некоторого количества переменных стандартных типов. В структуру, в противоположность массиву, можно объединять данные различных типов. Синтаксис структуры фактически идентичен синтаксису класса. Только структуры, как правило, используют в качестве объединения данных, а классы – в качестве объединения данных и функций.

Рассмотрим структуру, содержащую три поля и предназначенную для хранения информации о товарах, хранящихся на складе.

const int N=30;

struct Tovar {

char name[N]; //íàèìåíîâàíèå òîâàðà

int number; //íîìåð òîâàðà

float cena;

};// точка с запятой !

int main() {

Tovar t1;// новый тип данных

cout << "\nVvedite naimenovanie tovara: "; cin.get(t1.name, N);

cout << "Vvedite nomer tovara: "; cin >> t1.number;

cout << "Vvedite stoimost tovara: "; cin >> t1.cena;

cout << "\n\nNaimenovanie tovara: " << t1.name

<< " Nomer tovara: " << t1.number

<< " Stoimost tovara: " << t1.cena;

getch(); return 0;

}

В приведённой программе присутствуют три основных аспекта работы со структурами: определение структуры, определение переменной типа этой структуры и доступ к полям этой структуры. Само определение структуры (struct Tovar {};) не создаёт никаких переменных, т.е. не происходит ни выделения физической памяти, ни объявления переменной. В то же время определение обычной переменной предполагает выделение памяти под неё.

Когда происходит определение структурной переменной (Tovar t1;), то под структурную переменную всегда отводится столько памяти, сколько достаточно для хранения всех её полей.

Доступ к полям структуры выполняется с помощью операций выбора:

– . (точка) при обращении к полю через имя структуры;

– -> при обращении через указатель.

struct Tovar {

char name[N]; //íàèìåíîâàíèå òîâàðà

int number; //íîìåð òîâàðà

float cena;

};// точка с запятой !

Tovar t2, *pt;

t2.name="sanki derevyannye";

pt->cena=321.65; //или (*pt).cena=321.65;

 

Инициализацию полей переменной t1 можно произвести в момент её определения: Tovar t1 = { "sanky metal.", 3373, 217.55 };. Каждая из величин присваивается по очередности соответствующему полю. Также можно присваивать значение одной структурной переменной другой структурной переменной: t2 = t1; . Не допускается применение таких операций, как t3=t2+t1; .

 

Структуры и функции

Использование структурных переменных с функциями подчиняется правилам для обычных переменных стандартных типов. Структурные переменные можно передавать в функцию по значению, по ссылке или по указателю. Также допускается использование структурной переменной в качестве возвращаемого значения. При разделении логически законченных действий программы на функции, надо иметь в виду, что структура, в отличие от массива, может быть возвращаемым значением функции. Структура может передаваться в функцию по константной ссылке, а также и по значению, что несколько хуже, потому что в этом случае затрачивается время на копирование и требуется дополнительное место в стеке параметров.

 

Массивы структур

Массивы могут содержать в себе не только данные основных типов, но структуры. Структурный массив – довольно удобная конструкция для применнения на практике, поскольку позволяет объединить данные различных типов. Синтаксис структурных массивов ничем не отличается от синтаксиса массивов на основе встроенных основных типов данных. Массивы структур могут быть как константными, так и динамическими, например:

Tovar t[200];

Tovar *tm = new Tovar [N];

Рассмотрим пример программы, позволяющей создавать базу данных склада товаров и хранить её в текстовом файле. Каждая строка файла содержит: наименование товара, идентификационный номер товара, стоимость товара.

const int N=30;

struct Tovar {

char name[N]; //íàèìåíîâàíèå òîâàðà

int number; //íîìåð òîâàðà

float cena;

};

int main() {

int K=0, i;

cout << "Skolko zapisei vy hotite dobavit? "; cin >> K;

Tovar *t = new Tovar [K]; //äèíàìè÷åñêèé ìàññèâ ñòðóêòóðû

for(i=0; i<K; i++) {

cout << "\n Zapis N " << i << endl;

cout << "Vvedite naimenovanie tovara: "; cin >> t[i].name;

cout << "Vvedite nomer tovara: "; cin >> t[i].number;

cout << "Vvedite stoimost tovara: "; cin >> t[i].cena;

}

ofstream fout("c:\\tovar\\tovarbd.txt", ios::app);

if(!fout) { cout << "\nOshibka otkrytiya faila!"; getch(); return 1; }

for(i=0; i<K; i++) {

fout << t[i].name << ' ' << t[i].number << ' ' << t[i].cena;

fout << endl;

}

cout << "\nDannye zapisany v fail.";

getch(); return 0;

}

Рассмотрим следующий пример программы, позволяющей считывать текстовый файл базы товаров и выводить все записи в упорядоченном по цене виде (сортировка по цене).

const int N=30;

struct Tovar {

char name[N]; //íàèìåíîâàíèå òîâàðà

int number; //íîìåð òîâàðà

float cena;

};

void sortmp_po_cene(Tovar tmas[], int R);

 

int main() {

int K=0, i=0;

char sim;

ifstream fin("c:\\tovar\\tovarbd.txt");

if(!fin) { cout << "\nOshibka otkrytiya faila!"; getch(); return 1; }

while(!fin.eof()) { //îïðåäåëÿåì êîë-âî ñòðîê áàçû

fin.get(sim);

if(sim=='\n') K++;

}

K=K-1; //т.к. последняя строка пустая

Tovar *t = new Tovar [K];

fin.close();

ifstream fin2("c:\\os\\tovar\\tovarbd.txt");

if(!fin2) { cout << "\nOshibka otkrytiya faila!"; getch(); return 1; }

while(i<K) {

fin2 >> t[i].name >> t[i].number >> t[i].cena;

i++;

}

fin2.close();

sortmp_po_cene(t, K); //отсортируем структурный массив по цене (по возрастанию)

for(i=0; i<K; i++) {

cout << "\n Zapis N " << i << endl;

cout << t[i].name << " " << t[i].number << " " << t[i].cena;

cout << endl;

}

getch(); return 0;

}

//---------------------------------------------------------------------------

void sortmp_po_cene(Tovar tmas[], int R) {

for(int i=0; i<R-1; i++) {

for(int j=i+1; j<R; j++) {

if(tmas[j].cena < tmas[i].cena) {

Tovar a = tmas[i]; tmas[i] = tmas[j]; tmas[j] = a;

}

}

}

}

Здесь мы применили алгоритм сортировки методом пузырька. Сначала первый элемент массива сравнивается по очереди с остальными элементами (начиная со второго). Если он больше, чем какой либо из элементов массива, то эти элементы меняются местами. Следовательно, нам станет известен первый элемент последовательности, самый маленький. Затем сравниваем второй элемент по очереди со всеми остальными, начиная с третьего, и вновь меняем местами элементы, если найдётся элемент меньший, чем второй. В результате будет известен уже второй элемент последовательности. Этот процесс продолжается далее для всех остальных элементов до предпоследнего.

Приведённые примеры программ показали, что массив структуры очень удобная для применения конструкция.

Д/З Измените рассмотренную программу таким образом, чтобы она позволяла сортировать считанную из файла информацию по наименованию товара и перезаписывать данный файл. Напишите также функцию для вычисления стоимости всех товаров, указанных в базе.
Ответ void sortmp_po_name(Tovar tmas[], int R) { int i, j; for(i=0; i<R-1; i++) { for(j=i+1; j<R; j++) { if(tmas[j].name[0] < tmas[i].name[0]) { Tovar a = tmas[i]; tmas[i] = tmas[j]; tmas[j] = a; } } } ofstream fout("c:\\os\\tovar\\tovarbd_sort.txt"); if(!fout) { cout << "\nOshibka zapisi faila!"; getch(); return ; } for(i=0; i<R; i++) { fout << tmas[i].name << ' ' << tmas[i].number << ' ' << tmas[i].cena; fout << endl; } fout.close(); cout << "\n Fail otsortitovan i perezapisan!"; } //--------------------------------------------------------------------------- float sum_cena(Tovar tm[], int R) { float sumc=0; for(int i=0; i<R; i++) { sumc=sumc+tm[i].cena; } return sumc; }

 

Поиск в массиве структур

Рассмотрим пример программы, позволяющей осуществлять поиск по наименованию товара в базе товаров.

… то же самое

void poisk_po_name(Tovar *tb, int& R) {

char namet[N];

cout << "\nVvedite naimenovanie tovara: "; cin >> namet;

int l = strlen(namet);

cout << "\n Naideny sovpadeniya: " << endl;

for(int i=0; i<R; i++) {

if(strncmp(namet, tb[i].name, l)==NULL) { //сравнивает первую строку и

//l-символов второй строки

cout << tb[i].name << " " << tb[i].number << " " << tb[i].cena;

cout << endl;

}

}

}

Данная функция позволяет по нескольким первым буквам, указанным пользователем, определить наличие совпадений в базе и вывести их.

 

Вложенность структур

Структуры допускают вложенность, т.е. использование структурной переменной в качестве поля какой-либо другой структуры. Рассмотрим практический пример применения структур для решения следующей задачи.

Составим программу для расчёта площади комнаты, учитывая что придётся работать в английской системе мер, основными единицами измерения которой служат фут и дюйм, причём 1 фут равен 12 дюймам.

struct Distance { // Расстояния в английской системе

int feet; // футы

float inch; // дюймы

};

struct Ploshad { // Размеры для определения площади комнаты

Distance length;

Distance width;

};

int main() {

Ploshad komnata1;

komnata1.length.feet = 13; komnata1.length.inch = 6.5;

komnata1.width.feet = 10; komnata1.width.inch = 0.0;

float l1 = komnata1.length.feet + komnata1.length.inch / 12;

float w1 = komnata1.width.feet + komnata1.width.inch / 12;

cout << "Ploshad komnaty1 = " << l1*w1 << " (kv. futov).\n";

Ploshad komnata2 = {{13, 6.5},{10, 1.10}}; // инициализация размеров по длине и ширине

float l2 = komnata2.length.feet + komnata2.length.inch / 12;

float w2 = komnata2.width.feet + komnata2.width.inch / 12;

cout << "Ploshad komnaty2 = " << l2*w2 << " (kv. futov).\n";

return 0;

}

Преобразование целого типа поля feet к типу float происходит автоматически. При вычислении площади комнаты на самом деле осуществляется преобразование переменной типа Ploshad, хранящей два значения типа Distance, к одному вещественному значению. Возможность преобразования значений одного типа в значения другого типа является важным аспектом для программ, которые используют типы данных, определяемые пользователем.

Д/З Измените рассмотренную программу, создав вместо структуры Ploshad, структуру Volume для хранения трёх измерений помещения. Программа должна вычислять объём двух комнат и выводить результат на экран. Для одной комнаты размеры фиксированные, а для другой должны вводится пользователем.
Ответ struct Distance { // Расстояния в английской системе int feet; // футы float inch; // дюймы }; struct Volume { // Размеры для определения объёма комнаты Distance length; Distance width; Distance height; };   int main() { Volume komnata1 = {{13, 6.5},{10, 1.10}, {24.3, 2}}; float l1 = komnata1.length.feet + komnata1.length.inch / 12; float w1 = komnata1.width.feet + komnata1.width.inch / 12; float h1 = komnata1.height.feet + komnata1.height.inch / 12; cout << " Ob'em komnaty1 = " << l1*w1*h1 << " (kub. futov).\n";   Volume komnata2; cout << "\nVvedite dlinu komnaty2 v futax: "; cin >> komnata2.length.feet; cout << "Vvedite ostav. dlinu v inches: "; cin >> komnata2.length.inch; float l2 = komnata2.length.feet + komnata2.length.inch / 12; … cout << "\n Ob'em komnaty2 = " << l2*w2*h2 << " (kub. futov).\n"; return 0; }

 

Рекурсия

Рекурсивной называется функция, которая вызывает саму себя. Такая рекурсия называется прямой. Существует ещё косвенная рекурсия, когда две или более функций вызывают друг друга. Если функция вызывает себя, в стеке создаётся копия значений её параметров, как и при вызове обычной функции, после чего управление передаётся первому исполняемому оператору функции. При повторном вызове этот процесс повторяется. Ясно, что для завершения вычислений каждая рекурсивная функция должна содержать хотя бы одну нерекурсивную ветвь алгоритма, заканчивающуюся оператором возврата. При завершении функции соответствующая часть стека освобождается, и управление передаётся вызывающей функции, выполнение которой продолжается с точки, следующей за рекурсивным вызовом.

Классическим примером рекурсивной функции является вычисление факториала числа (n!=n*(n-1)!, где 0!=1 и 1!=1):

unsigned long factor(unsigned long n) {

if (n==0 || n==1) return 1;

return (n*factor(n-1));

}

int main() {

int n;

cout << "Vvedite chislo: "; cin >> n;

cout << "Factorial " << n << " raven " << factor(n);

return 0;

}

Рекурсивные функции чаще всего применяют для компактной реализации рекурсивных алгоритмов, а также для работы со структурами данных, описанными рекурсивно, например с двоичными деревьями. Любую рекурсивную функцию можно реализовать без применения рекурсии. Для этого необходимо обеспечить хранение всех необходимых данных самостоятельно. Достоинством рекурсии является компактная запись, а недостатками – расход времени и памяти на повторные вызовы функции и передачу ей копий параметров, и, главное, опасность переполнения стека.

 

Алгоритм быстрой сортировки

В прошлом семестре мы рассматривали сортировку массива методом выбора. Это наиболее простой метод, который характеризуется квадратичной зависимостью времени сортировки t от количества элементов N:

t = a×N2 + b×N×lgN,

где a и b – константы, зависящие от программной реализации алгоритма. Иными словами, для сортировки массив требуется просмотреть порядка N раз. Существуют алгоритмы и с лучшими характеристиками, самый известный из которых реализован в методе быстрой сортировки.

Идея алгоритма быстрой сортировки состоит в следующем. Применим к массиву процедуру разделения относительно среднего элемента. Процедура разделения делит массив на две части. В левую помещаются элементы, меньшие, чем элемент, выбранный в качестве среднего, а в правую – большие. Это достигается путём просмотра массива попеременно с обоих концов, при этом каждый элемент сравнивается с выбранным средним, и элементы, находящиеся в неподходящей части, меняются местами. После завершения процедуры разделения средний элемент оказывается на своём окончательном месте. Далее процедуру разделения необходимо повторить отдельно для левой и правой части: в каждой части выбирается среднее, относительно которого она делится на две, и так далее.

Рассмотрим этот алгоритм на примере программы упорядочивания массива методом быстрой сортировки, используя рекурсию, поскольку по своей сути алгоритм является рекурсивным, так как на каждом шаге образовываются две половинки текущего фрагмента и к ним снова нужно применять процедуру разделения.

int main() {

Формирование массива.

l = 0; r = n - 1; // левая и правая границы начального фрагмента

qsort(arr, l, r);

Вывод отсортированного массива.

return 0; }

//---------------------------Рекурсивная функция быстрой сортировки:

void qsort(int* array, int left, int right) {

int i = left, j = right; // устанавливаем начальные значения

int middle = array[(left + right) / 2]; // выбирается средний элемент

int temp;

while (i < j) {

while (array[i] < middle) i++; // продвижение по массиву слева направо

while (middle < array[j]) j--; // продвижение справа налево

if (i <= j) {

temp = array[i];

array[i] = array[j];

array[j] = temp;

i++; j--;

}

}

if (left < j) qsort(array, left, j); //сортировка левой половинки текущего фрагмента

if (i < right) qsort(array, i, right); //сортировка правой половинки текущего фрагмента

}

 

Линейные списки

Самый простой способ связать множество элементов – сделать так, чтобы каждый элемент содержал ссылку на следующий. Такой список называется однонаправленным (односвязным). Если добавить в каждый элемент вторую ссылку на предыдущий элемент, получится двунаправленный список (двусвязный), если последний элемент связать указателем с первым, получится кольцевой список.

Пример структуры двунаправленного линейного списка:

struct DvSpisok {

int d;

Node *next; //указатель на следующий элемент

Node *pred; //указатель на предыдущий элемент

};

Каждый элемент списка содержит ключ, идентифицирующий этот элемент. Ключ обычно бывает либо целым числом, либо строкой и является частью поля данных. Например, если создаётся линейный список из записей, содержащих фамилию, год рождения, стаж работы и пол, любая часть записи может выступать в качестве ключа: при упорядочивании списка по алфавиту ключом будет фамилия, а при поиске, к примеру, ветеранов труда ключом будет стаж. Над списками можно выполнять следующие операции:

– начальное формирование списка (создание первого элемента);

– добавление элемента в конец списка;

– чтение элемента с заданным ключом;

– вставка элемента в заданное место списка (до или после элемента с заданным ключом);

– удаление элемента с заданным ключом;

– упорядочивание списка по ключу.

Сортировка связанного списка заключается в изменении связей между элементами. Алгоритм состоит в том, что исходный список просматривается, и каждый элемент вставляется в новый список на место, определяемое значением его ключа.

Стеки

Стеком называется структура данных, в которой элемент, занесённый первым, извлекается последним. Стек реализует принцип обслуживания LIFO (последним пришёл – первым ушёл). В алгоритме быстрой сортировки стек используется для хранения границ неупорядоченных фрагментов. Стек – это частный случай однонаправленного списка, добавление элементов в который и выборка из которого выполняются с одного конца, называемого вершиной стека. Другие операции со стеком не определены. При выборке элемент исключается из стека. Стеки широко применяются в системном ПО, компиляторах и в различных рекурсивных алгоритмах.

   

Рассмотрим программу, которая формирует стек из пяти целых чисел (1, 2, 3, 4, 5) и выводит его на экран. Функция помещения в стек по традиции называется push, а выборки из стека - pop. Указатель top для работы со стеком всегда ссылается на его вершину.

Итак, опишем структуру, зная, что каждый элемент стека должен содержать целое число и указатель на следующий элемент:

struct Stack {

int d;

Stack *p;

};

//---------------------------Начальное формирование стека:

Stack* first(int d)

{

Stack *pv = new Stack; // Описываем вспомогательную переменную-указатель pv и заносим // в неё адрес нового элемента стека, который создаётся с помощью операции new.

pv->d = d; pv->p = 0; // занесение первого элемента в стек

return pv;

}

//---------------------------Занесение в стек:

void push(Stack **top, int d) // в функцию передаются элементы стека и указ. на вершину

{

Stack *pv = new Stack;

pv->d = d; //информационное поле структуры заполняется переданными в функцию значениями.

pv->p = *top; // новый элемент становится вершиной стека и поле его указателя ссылается

*top = pv; // на ранее помещённый в стек элемент.

}

Выборка из стека выполняется примерно аналогично. Сначала из вершины стека выбирается указатель на его следующий элемент, который станет новой вершиной стека. Этот указатель является возвращаемым значением функции.

//---------------------------Выборка из стека:

int pop(Stack **top)

{

int temp = (*top)->d;

Stack *pv = *top;

*top = (*top)->p;

delete pv; // удаление элемента

return temp;

}

//---------------------------Главная функция:

int main(int argc, char* argv[])

{

Stack *top = first(1); // занесение первого элемента в стек

for (int i = 2; i<6; i++) push(&top, i); // занесение в стек элементов, путём передачи этих элементов и указателя на вершину стека в функцию push.

while (top) cout << pop(&top) << " ";

return 0;

}

Результат работы программы: 5 4 3 2 1

Очереди

Очередь – это частный случай однонаправленного списка, добавление элементов в который выполняется в один конец, а выборка – из другого конца. Таким образом, для очереди определены всего две операции: помещение в конец и выборка из начала. При выборке элемент удаляется из очереди (аналогично стеку и в противоположность списку). Очередь реализует принцип обслуживания FIFO (первым пришёл – первым ушёл). В программировании очереди применяются, например при диспетчеризации задач ОС, буферизованном вводе/выводе.

Рассмотрим программу, которая формирует очередь из пяти целых чисел и выводит её на экран. Указатель на начало очереди назовём pbeg, а указатель на конец – pend.

struct Node {

int d;

Node *p;

};

//---------------------------Начальное формирование очереди:

Node* first(int d)

{

Node *pv = new Node;

pv->d = d; pv->p = 0;

return pv;

}

//---------------------------Добавление в конец:

void add(Node **pend, int d)

{

Node *pv = new Node;

pv->d = d; pv->p = 0;

(*pend)->p = pv;

*pend = pv;

}

//---------------------------Выборка:

int del(Node **pbeg)

{

int temp = (*pbeg)->d;

Node *pv = *pbeg;

*pbeg = (*pbeg)->p;

delete pv;

return temp;

}

//---------------------------Главная функция:

int main(int argc, char* argv[])

{

Node *pbeg = first(1);

Node *pend = pbeg;

for(int i=2; i<6; i++) add(&pend, i);

while(pbeg) cout << del(&pbeg) << ' ';

return 0;

}

Результат работы программы: 1 2 3 4 5 .

Из кода этой программы видно, что у неё очень много общего с предыдущей. Функции начального формирования очереди и стека, а также функции выборки абсолютно идентичны. Функция first формирует первый элемент очереди и возвращает указатель на него. Функция add выполняет добавление в конец, поэтому ей передаётся указатель на конец очереди и элемент, который следует добавить. Выборка выполняется из начала очереди, при этом указатель на её начало изменяется.

 

Контрольная работа

 

Пусть существует программа, которая считывает записи из бинарного файла и формирует динамический массив структуры Tovar. Напишите часть главной функции, которая в зависимости от желания пользователя, позволяет вызывать функцию редактирования записей. Напишите эту функцию редактирования, которая позволяет редактировать данные указанного пользователем товара и по желанию пользователя сохранять изменения (перезаписывать файл).

int main() {

cout << "\n Budete redaktirovat?(y/n): "; cin >> ans;

if(ans=='y') redakt_po_name(ma, KZ);

cout << "\n Sohranit izmeneniya?(y/n): "; cin >> ans;

if(ans=='y') savefile(ma, KZ, dlz);

}

void redakt_po_name(Avia *tb, int& N) {

char namer[R];

cout << "\n Ukagite naimenovanie reisa: "; cin >> namer;

int l = strlen(namer);

int ks=0; //êîë-âî ñîâïàäåíèé

for(int i=0; i<N; i++) {

if(strncmp(namer, tb[i].reis, l)==NULL) {

cout << "\nVvedite novoe imya reisa: "; cin >> tb[i].reis;

cout << "\nVvedite tip samoleta: "; cin >> tb[i].tips;

cout << "\nVvedite kol. prod. biletov: "; cin >> tb[i].kpb;

cout << "\nVvedite stoimost bileta: "; cin >> tb[i].cb;

ks++;

}

}

if(ks==0) { cout << "\n Takogo reisa net !"; }

}

//---------------------------------------------------------------------------

void savefile(Avia m[], int M, int dz) {

ofstream fout("c:\\os\\aviabase3_red.dat", ios::binary);

if(!fout) { cout << "\n Oshibka zapisi v fail!"; getch(); return ; }

fout.write(reinterpret_cast<char*>(m), M*dz);

fout.close();

}

 

Лекция 4(14,5 стр.)

Конструкторы

Как правило, удобнее инициализировать поля объекта автоматически в момент его создания, а не явно вызывать в программе соответствующий метод. Такой способ инициализации реализуется с помощью особого метода класса, называемого конструктором. Конструктор – это метод класса, предназначенный для инициализации объекта и выполняющийся автоматически в момент создания объекта. У конструкторов есть несколько особенностей. Во-первых, имя конструктора в точности совпадает с именем класса, что позволяет компилятору отличить конструктор от других методов класса. Во-вторых, у конструкторов не существует возвращаемого значения (даже типа void). Это объясняется тем, что конструктор автоматически вызывается системой, и, следовательно, не существует вызывающей программы или функции, которой конструктор мог бы возвратить значение.

Приведём пример, в котором вместо метода setdata() используется два конструктора (без аргументов и с аргументами).

class Tovar {

private:

enum { N = 30 }; // static const int N = 30;

char name[N];

int number;

float cena;

public:

Tovar() {

//name[0]='\0'; или strcpy(name,"\0"); number = 0; cena=0.;

cout << "\n Konstruktor 1" << endl;

}

Tovar(char* tname, int tnum, float tcen) {

strcpy(name, tname);

number = tnum;

cena = tcen;

cout << "\n Konstruktor 2" << endl;

}

void getdata() {

}

void showdata() {

}

};

int main() {

Tovar t1("Sanki der.", 1100, 231.45), t2;

t2.getdata();

//t2=t1; //такая операция допускается

t1.showdata(); t2.showdata();

getch(); return 0;

}

В данном примере, прослеживается такое явление, как перегрука конструкторов. Перегрузка уже встречалась нам при изучении функций. Компилятор определяет требуемый для запуска конструктор по числу и характеру (типу) аргументов. Для объекта t1 запускается конструктор 2, а для t2 – конструктор 1.

Рассмотрим другой пример, демонстрирующий использование альтернативного синтаксиса конструкторов, определение методов вне класса и использование объектов в качестве аргументов функций. Предположим, необходимо создать инженерный проект с использованием английской системы мер. Было бы удобно представлять длины в виде двух чисел, одно из которых равнялось бы количеству футов, а другое – количеству дюймов.

class Distance {

private:

int feet;

float inch;

public:

Distance(): feet(0), inch(0.0) // êîíñòðóêòîð áåç àðãóìåíòîâ

{ }

Distance(int ft, float in): feet(ft), inch(in) // êîíñòðóêòîð ñ äâóìÿ àðãóìåíòàìè

{ }

void getdist() { // ââîä äëèíû ïîëüçîâàòåëåì

cout << "\n Vvedite chislo futov: "; cin >> feet;

cout << " Vvedite chislo duymov: "; cin >> inch;

}

void showdist() { // âûâîä äëèíû íà ýêðàí

cout << feet << "'-" << inch << "''";

}

void add_dist(Distance a1, Distance a2); // ïðîòîòèï

};

//-----Îïðåäåëåíèå ìåòîäîâ êëàññà âíå êëàññà (ñëîæåíèå äëèí)

void Distance::add_dist(Distance a1, Distance a2) {

inch = a1.inch + a2.inch; // Ñëîæåíèå äþéìîâ

feet = 0; // ñ âîçìîæíûì çà¸ìîì

if(inch >= 12.0) { // Åñëè ÷èñëî äþéìîâ áîëüøå 12.0, òî

inch = inch - 12.0; // óìåíüøàåì ÷èñëî äþéìîâ íà 12.0 è

feet++; // óâåëè÷èâàåì ÷èñëî ôóòîâ íà 1.

}

feet += a1.feet + a2.feet; // Ñëîæåíèå ôóòîâ

}

//---------------------------------------------------------------------------

int main() {

Distance d1, d2(11, 6.25), d3; // îïðåäåëåíèå è èíèöèàëèçàöèÿ d2

d1.getdist(); // ââîä çíà÷åíèé äëÿ d1

d3.add_dist(d1, d2); // d3=d1+d2

cout << "\n d1 = "; d1.showdist();

cout << "\n d2 = "; d2.showdist();

cout << "\n d3 = "; d3.showdist();

getch(); return 0;

}

В этом примере приведён альтернативный синтаксис конструкторов, который аналогичен уже рассматривавшемуся ранее и более эффективному:

Distance() {

feet = 0; inch = 0;

}

Distance(int ft, float in) {

feet = ft;

inch = in;

}

Основной блок этой программы начинается с присвоения начальных значений полям объекта d2 класса Distance, после чего производится его сложение с экземпляром d1, инициализируемым пользователем. На экран выводится следующее:

Vvedite chislo futov: 11

Vvedite chislo duymov: 6.25

d1 = 11'-6.25"

d2 = 11'-6.25"

d3 = 23'-0.5"

Перегруженные конструкторы

Из программы видно, что инициализация переменной d2 класса Distance производится в момент её создания, путём вызова конструктора с аргументами:

Distance(int ft, float in): feet(ft), inches(in) // конструктор с двумя аргументами

Здесь мы инициализируем поля feet, inches теми значениями, которые передаются конструктору в качестве аргументов.

Если же для нас важно, какими значениями будут инициализироваться поля объектов класса (d1 и d3) при объявлении, то следует явно определить конструктор, что и сделано в рассматриваемой программе:

Distance(нет аргументов): feet(0), inches(0.0) { } // конструктор без аргументов

Здесь члены класса инициализируются константными значениями (0), что позволяет быть уверенным в не случайности значений.

Итак, в программе имеется два явно определённых конструктора с одним и тем же именем Distance(). В этом случае говорят, что конструктор является перегруженным. Какой из этих двух конструкторов исполняется во время создания нового объекта, зависит от того, сколько аргументов используется при вызове. Спроси у студентов, какие конструкторы вызываются:

Distance dlina; // вызывает 1-ый конструктор

Distance shirina(11, 6.2); // вызывает 2-ой конструктор

 

Структуры и классы

Все примеры, рассмотренные нами до сих пор, подтверждали негласный принцип: структуры предназначены для объединения данных, а классы – для объединения данных и функций. На самом деле разница между структурами и классами заключается лишь в том, что по умолчанию все члены класса являются скрытыми, а все члены структуры – открытыми. Но при изменении структуры, можно добиться её эквивалентности с классом (см. табл.).

class F { private: int data1; public: void func(); }; struct F { void func(); private: int data1; };

 

Классы и память

В процессе изучения классов и объектов может сформироваться представление об объекте как о копии класса с точки зрения внутренней структуры. На самом деле всё устроено несколько сложнее. Каждый объект имеет собственные независимые поля данных. С другой стороны, все объекты одного класса используют одни и те же методы. Методы класса создаются и помещаются в память компьютера всего один раз – при создании класса. Это вполне оправданно, поскольку нет никакого смысла держать в памяти копии методов для каждого объекта данного класса, так как у всех объектов методы одинаковы. А поскольку наборы значений полей у каждого объекта свои, поля объектов не должны быть общими. Это значит, что при создании объектов каждый из наборов данных занимает определённую совокупность свободных мест в памяти.

Статические данные класса

Если поле данных класса описано с ключевым словом static, то значение этого поля будет одинаковым для всех объектов данного класса. Статические данные класса полезны в тех случаях, когда необходимо, чтобы все объекты включали в себя какое-либо одинаковое значение. Статическое поле видимо только внутри класса, но время его жизни совпадает со временем жизни программы. В отличие от статической переменной функции, предназначенной для сохранения значения между вызовами, статическая переменная класса используется для хранения данных, совместно используемых объектами класса.

Для понимания необходимости использования статических переменных класса, представим следующую ситуацию. Допустим необходимо, чтобы объект вашего класса располагал информацией, сколько ещё объектов этого же класса существует на данный момент времени в памяти. В этом случае можно включить в класс статическую переменную count, которая будет доступна всем объектам класса.

class Stat {

private:

static int count; // îáùåå ïîëå äëÿ âñåõ îáúåêòîâ

public:

Stat() { count++; } // èíêðåìåíòèðîâàíèå ïðè ñîçäàíèè îáúåêòà

int getcount() { return count; }

};

int Stat::count = 0; // îïðåäåëåíèå count необходимо делать именно вне класса

//---------------------------------------------------------------------------

int main() {

Stat f1, f2, f3; //ñîçäàíèå òð¸õ îáúåêòîâ

cout << "Chislo ob'ektov: " << f1.getcount() << endl;

cout << "Chislo ob'ektov: " << f2.getcount() << endl;

cout << "Chislo ob'ektov: " << f3.getcount() << endl;

getch(); return 0;

}

В этом примере конструктор класса при создании объекта класса будет инкрементировать значение поля count. В главной функции мы определяем три объекта класса Stat. Вопрос: Сколько раз происходит инкрементирование поля count, и каков будет вывод программы? Ответ: Поскольку конструктор в этом случае вызывается трижды, инкрементирование поля count также происходит трижды. Метод getcount() возвращает значение count. Метод вызывается для каждого из объектов, и во всех случаях он возвращает одну и ту же величину: 3 (если бы мы использовали не статическое, а автоматическое поле count, то конструктор увеличивал бы на единицу значение этого поля для каждого объекта, и результатом вывода программы была бы 1).

Рассмотрим теперь строку: int Stat::count = 0;. Определение статических полей класса происходит не так, как для обычных полей. Обычные поля объявляются и определяются при помощи одного оператора. Для статических полей эти два действия выполняются двумя разными операторами: объявление поля находится внутри определения класса, а определение поля, как правило, располагается вне класса и зачастую представляет собой определение глобальной переменной. Помещая определение статического поля вне класса, мы обеспечиваем однократное выделение памяти под это поле до того, как программа будет запущена на выполнение и статическое поле в этом случае станет доступным всему классу.

Константные методы

Вы уже сталкивались с применением модификатора const для защиты значений переменных от изменения. Можно сделать методы класса константными. Константные методы отличаются тем, что не изменяют значений полей своего класса. Для того, чтобы сделать функцию константной, необходимо указать ключевое слово const после прототипа функции, но до начала тела функции. Если объявление и определение функции разделены, то модификатор const необходимо указывать дважды – как при объявлении функции, так и при её определении. Те методы, которые лишь считывают данные из поля класса, имеет смысл делать константными, поскольку у них нет необходимости изменять значения полей объектов класса.

На примере улучшения предыдущей программы, рассмотрим теперь способ применения модификатора const с методами класса и их аргументами.

class Distance {

… идентично предыдущей программе

void showdist() const // вывод длины на экран

{ cout << feet << "'-" << inches << "''"; }

Distance plus(const Distance&) const; // прототип

};

//---------------------------------------------------------------------------

Distance Distance:: plus (const Distance& a2) const

{

… идентично предыдущей программе

}

//---------------------------------------------------------------------------

int main()

{

… идентично предыдущей программе

}

В данной программе мы передаём аргумент в функцию (plus(const Distance&а2)) по ссылке (экономим память, т.к. ссылка это псевдоним) и хотим быть уверенными в том, что функция не изменит значения этого аргумента, в качестве которого при вызове выступает переменная d2 главной функции. Для этого параметр а2 функции plus(const Distance& а2) указывается с модификатором const в её объявлении и определении.

Модификатор const можно применять и для объектов класса (например, const Distance pole(300,0)). Если объект класса объявлен с модификатором const, он становится недоступным для изменения. Это означает, что для такого объекта можно вызывать только константные методы, поскольку только они гарантируют, что объект не будет изменён.

Деструкторы

Как мы уже видели, особый метод класса – конструктор – вызывается при создании объекта. Следовательно, можно догадаться, что существует другая функция, автоматически вызываемая при уничтожении объекта и называемая деструктором. Деструктор имеет имя, совпадающее с именем конструктора (а следовательно, и с именем класса) и предваряющееся символом ~ (тильда): class Primer {

private:

int data;







Последнее изменение этой страницы: 2016-04-08; Нарушение авторского права страницы

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.94.202.172 (0.106 с.)