12+
Языки, технологии и методы программирования

Бесплатный фрагмент - Языки, технологии и методы программирования

Объем: 104 бумажных стр.

Формат: epub, fb2, pdfRead, mobi

Подробнее

Введение

Неоднократные попытки создания универсального языка программирования не привели к успеху: люди не только продолжают использовать старые языки, но и постоянно придумывают новые. Поэтому изучать приходится несколько языков программирования. При этом набор постоянно используемых языков со временем изменяется: сначала нельзя было обойтись без знания Фортрана, затем необходим был ТурбоПаскаль, сейчас трудно обойтись без знания С++. Что будет завтра? Автор затрудняется ответить на этот вопрос, но уверен в том, что «фаворит» снова поменяется.

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

Данный учебник основан на курсах «Языки программирования», «Методы программирования» и «Программирование для мобильных платформ» читаемых студентам факультета компьютерных технологий Комсомольского-на-Амуре государственного университета. Цель учебника — объяснить основные понятия, конструкции, принципы разработки и реализации современных языков индустриального программирования.

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

1 Перегрузка и шаблоны функций

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

Компилятор определяет, какую именно функцию требуется вызвать, по типу фактических параметров. Этот процесс называется разрешением перегрузки (перевод английского слова resolution в смысле «уточнение»). Тип возвращаемого функцией значения в разрешении не участвует. Механизм разрешения основан на достаточно сложном наборе правил, смысл которых сводится к тому, чтобы использовать функцию с наиболее подходящими аргументами и выдать сообщение, если такой не найдется. Допустим, имеется четыре варианта функции, определяющей наибольшее значение:

/ / Возвращает наибольшее из двух целых:

int max (int. int):

// Возвращает подстроку наибольшей длины:

char* max (char*. char*);

// Возвращает наибольшее, из первого параметра и длины второго:

int max (int, char*);

// Возвращает наибольшее из второго параметра и длины первого:

int max (char*, int);

void f (int a. int b. char* c. char* d) {

cout « max (a, b) « max (c. d) « max (a. c) « max (c, b);

}

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

Если точного соответствия не найдено, выполняются продвижения порядковых типов в соответствии с общими правилами, например,

bool и char в int, float в double и т. п. Далее выполняются стандартные преобразования типов, например, int в double или указателей в void*. Следующим шагом является выполнение преобразований типа, заданных пользователем, а также поиск соответствий за счет переменного числа аргументов функций. Если соответствие на одном и том же этапе может быть получено более чем одним способом, вызов считается неоднозначным и выдается сообщение об ошибке.

Неоднозначность может появиться при:

•преобразовании типа;

•использовании параметров-ссылок;

•использовании аргументов по умолчанию.

Пример неоднозначности при преобразовании типа:

#inclucle <iostream. h>

float f (float i) {

cout « «function float f (float 1)» « endl;

return i:

}

double f (double i) {

cout « «function double f (double i) " « endl:

return i*2:

}

int main () {

float x = 10.09:

double у = 10.09:

cout « f (x) « endl: // Вызывается f (float)

cout « f (y) « endl: // Вызывается f (double)

/* cout « f (10) « endl: Неоднозначность — как преобразовать 10: во float

или double? */

return 0:

}

Для устранения этой неоднозначности требуется явное приведение типа для константы 10.

Пример неоднозначности при использовании параметров-ссылок: если одна из перегружаемых функций объявлена как int f (int а. int b), а другая — как int f (int а. int &b), то компилятор не сможет узнать, какая из этих функций вызывается, так как нет синтаксических различий между вызовом функции, которая получает параметр по значению, и вызовом функции, которая получает параметр по ссылке.

Пример неоднозначности при использовании аргументов по умолчанию:

#include <iostream. h>

int f (int a) {return a:}

int f (int a. int b = l) {return a * b:}

int main () {

cout « f (10. 2): // Вызывается f (int. int)

/* cout « f (10): Неоднозначность — что вызывается: f (int, int) или

f (int)? */

return 0:

}

Ниже приведены правила описания перегруженных функций.

Перегруженные функции должны находиться в одной области видимости,

иначе произойдет сокрытие аналогично одинаковым именам переменных во

вложенных блоках.

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

•В различных вариантах перегруженных функций может быть различное количество параметров по умолчанию.

•Функции не могут быть перегружены, если описание их параметров отличается только модификатором const или использованием ссылки (например, int и const int или int и int&).

Многие алгоритмы не зависят от типов данных, с которыми они работают (классический пример — сортировка). Естественно желание параметризовать алгоритм таким образом, чтобы его можно было использовать для различных типов данных. Первое, что может прийти в голову — передать информацию о типе в качестве параметра (например, одним параметром в функцию передается указатель на данные, а другим — длина элемента данных в байтах). Использование дополнительного параметра означает генерацию дополнительного кода, что снижает эффективность программы, особенно при рекурсивных вызовах и вызовах во внутренних циклах; кроме того, отсутствует возможность контроля типов. Другим решением будет написание для работы с различными типами данных нескольких перегруженных функций, но в таком случае в программе будет несколько одинаковых по логике функций, и для каждого нового типа придется вводить новую.

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

Формат простейшей функции-шаблона:

template <class Туре> заголовок {

/* тело функции */

}

Вместо слова Туре может использоваться произвольное имя.

В общем случае шаблон функции может содержать несколько параметров, каждый из которых может быть не только типом, но и просто переменной, например:

template <class А. class В. int 1> void f () {…}

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

template <class Туре>

void sort__vybor (Type *b. int n) {

Type a; //буферная переменная для обмена элементов

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

int imin = i;

for (int j = i +1; j <n: j++)

i f (b [j] <b [imin]) imin = j;

а = Ь [i]; Ь [i] = Ь [imin]; b [imin] = а;

}

}

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

finclude <iostream. h>

template <class Туре> void sort_vybor (Type *b. int n):

int main () {

const int n = 20;

int i. b [n];

for (i = 0; i <n; 1++) cin» b [i]:

sort_vybor (b. n): // Сортировка целочисленного массива

for (i = 0; i <n; 1++) cout « b [i] « ` `:

cout « endl;

double a [] = {0.22. 117. -0.08. 0.21. 42.5};

sort_vybor (a. 5); // Сортировка массива вещественных чисел

for (i = 0; i <5; i++) cout « а [1] « ` `;

return 0;

}

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

Пример явного задания аргументов шаблона при вызове:

tempiate <class X. class Y. class Z> void f (Y. Z);

void g () {

f <int. char*. double> («Vasia». 3.0);

f <int. char*> («Vasia». 3.0);// Z определяется как double

f <int> («Vasia». 3.0); // Y определяется как char*. a Z — как double

// f («Vasia». 3.0); ошибка: X определить невозможно

}

Чтобы применить функцию-шаблон к типу данных, определенному пользователем (структуре или классу), требуется перегрузить операции для этого типа данных, используемые в функции.

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

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

void sort_vibor <int> (int *b. int n) {

…// Тело специализированного варианта функции

}

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

2 ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ

Любая программа предназначена для обработки данных, от способа организации, которых зависят алгоритмы работы, поэтому выбор структуры данных должен предшествовать созданию алгоритмов. Выше были рассмотрены стандартные способы организации данных, предоставляемые языком C++, — основные и составные типы. Наиболее часто в программах используются массивы, структуры и их сочетания, например, массивы структур, полями которых являются массивы и структуры.

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

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

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

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

struct Node {

Data d; // тип данных Data должен быть определен ранее

Node *p;

};

Стек — это динамическая структура данных, добавление элементов в которую и выборка из которой выполняется с одного конца, называемого вершиной стека. При выборке элемент исключается из стека. Говорят, что стек реализует принцип обслуживания LIFO (Last In — First Out, последним пришел — первым ушел). Стек проще всего представить себе, как закрытую с одного конца узкую трубку, в которую бросают мячи. Достать первый брошенный мяч можно только после того, как вынуты все остальные. Стеки широко применяются в системном программировании, компиляторах, в различных рекурсивных алгоритмах.

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

#include <iostream>

using namespace std;

struct StackNode {

int value;

StackNode *next;

};

StackNode *top;

void push (int value)

{

StackNode *temp = new StackNode;

temp-> value = value;

temp-> next = top;

top = temp;

}

bool isEmpty ()

{

return top == NULL;

}

int pop ()

{

int answer = top-> value;

top = top-> next;

return answer;

}

int main ()

{

for (int i = 1; i <6; i++)

{

push (i);

}

while (!isEmpty ())

{

cout <<pop () <<endl;

}

system («pause»);

return 0;

}

Результат работы программы выше:

5

4

3

2

1

Обратите внимание, что в стек числа заносились в порядке возрастания (от 1 до 5), а выводятся они в обратном порядке (от 5 до 1). Это демонстрирует способ хранения данных в стеке.

Очередь

Очередь — это динамическая структура данных, добавление элементов в которую выполняется в один конец, а выборка — из другого конца. При выборке элемент исключается из очереди. Говорят, что очередь реализует принцип обслуживания FIFO (First In — First Out, первым пришел — первым ушел). Очередь проще представить себе, постояв в ней час-другой (в магазине у кассы). В программировании очереди применяются, например при моделировании, диспетчеризации задач операционной системой, буферизированном вводе/выводе.

Ниже приведена программа, которая формирует очередь из пяти целых чисел и выводит ее на экран. Функция помещения в конец очереди называется enQueue, выборки — deQueue. Метод isEmpty служит для определения пуста ли очередь. Указатель на начало очереди называется first, указатель на конец — last.

#include <iostream>

using namespace std;

struct QueueNode {

int value;

QueueNode *next;

};

QueueNode *first, *last;

void enQueue (int value)

{

QueueNode *temp = new QueueNode;

temp-> value = value;

if (first == NULL)

first = last = temp;

else

{

last-> next = temp;

last = temp;

}

}

bool isEmpty ()

{

return first == NULL;

}

int deQueue ()

{

int answer = first-> value;

if (first == last)

first = last = NULL;

else

first = first-> next;

return answer;

}

int main ()

{

for (int i = 1; i <6; i++)

{

enQueue (i);

}

while (!isEmpty ())

{

cout <<deQueue () <<endl;

}

system («pause»);

return 0;

}

Результат работы программы выше

1

2

3

4

5

Обратите внимание, что в очередь числа заносились в порядке возрастания (от 1 до 5), и выводятся они в том же порядке (от 1 до 5). Это демонстрирует способ хранения данных в очереди.

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

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

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

Над списками можно выполнять следующие операции:

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

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

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

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

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

· и т. д.

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

struct ListNode {

int value;

ListNode *next;

ListNode *prev;

};

Ниже приведена программа, которая формирует список из 5 чисел, добавляет число в список, удаляет число из списка и выводит список на экран. Указатель на начало списка обозначен first, на конец — last. Ключом выступает поле value.

#include <iostream>

using namespace std;

struct ListNode {

int value;

ListNode *next = NULL;

ListNode *prev = NULL;

};

ListNode *first = NULL, *last = NULL;

void addToEnd (int value)

{

ListNode *temp = new ListNode;

temp-> value = value;

if (first == NULL)

first = last = temp;

else

{

temp-> prev = last;

last-> next = temp;

last = temp;

}

}

int addAfter (int value, int keyAfter)

{

ListNode *t = first;

while (t!= NULL)

{

if (t-> value == keyAfter)

break;

t = t-> next;

}

if (t == NULL)

return -1;

ListNode *temp = new ListNode;

temp-> value = value;

ListNode *tnext = t-> next;

//Вставлять элемент temp нужно между t и tnext

temp-> prev = t;

t-> next = temp;

if (tnext!= NULL)

{

temp-> next = tnext;

tnext-> prev = temp;

}

return 0;

}

int deleteItem (int key)

{

ListNode *t = first;

while (t!= NULL)

{

if (t-> value == key)

break;

t = t-> next;

}

if (t == NULL)

return -1;

ListNode *tprev = t-> prev;

ListNode *tnext = t-> next;

if (tprev == NULL && tnext == NULL)

{

//Удаляется единственный элемент

first = last = NULL;

return 0;

}

//Удалять элемент t нужно между tprev и tnext

if (tprev!= NULL)

tprev-> next = tnext;

else

first = first-> next;

if (tnext!= NULL)

tnext-> prev = tprev;

else

last = last-> prev;

return 0;

}

void print ()

{

ListNode *t = first;

while (t!= NULL)

{

cout <<t-> value <<endl;

t = t-> next;

}

}

int main ()

{

for (int i = 1; i <6; i++)

{

addToEnd (i);

}

addAfter (10, 2); //вставить 10 после элемента 2

deleteItem (4);

print ();

system («pause»);

return 0;

}

Результат работы программы выше

1

2

10

3

5

Бинарные деревья

Бинарное дерево — это динамическая структура данных, состоящая из узлов, каждый из которых содержит, кроме данных, не более двух ссылок на различные бинарные деревья. На каждый узел имеется ровно одна ссылка. Начальный узел называется корнем дерева. Узел, не имеющий поддеревьев, называется листом. Исходящие узлы называются предками, входящие — потомками. Высота дерева определяется количеством уровней, на которых располагаются его узлы.

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

Для дерева поиска определены операции:

· включения узла в дерево;

· обхода дерева.

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

#include <iostream>

using namespace std;

struct TreeNode {

int value;

TreeNode *leftChild = NULL;

TreeNode *rightChild = NULL;

};

TreeNode *top = NULL;

void add (int value)

{

TreeNode *temp = new TreeNode;

temp-> value = value;

if (top == NULL)

top = temp;

else

{

TreeNode *t = top;

while (true)

{

if (value <t-> value)

{

if (t-> leftChild!= NULL)

t = t-> leftChild;

else

{

t-> leftChild = temp;

return;

}

}

else

{

if (t-> rightChild!= NULL)

t = t-> rightChild;

else

{

t-> rightChild = temp;

return;

}

}

}

}

}

void pass (TreeNode *t)

{

if (t-> leftChild!= NULL)

pass (t-> leftChild);

cout <<t-> value <<endl;

if (t-> rightChild!= NULL)

pass (t-> rightChild);

}

int main ()

{

int mas [] = {10, 1, 5, 6, 2, 9, 7, 8, 4, 3};

for (int i = 0; i <10; i++)

{

add (mas [i]);

}

pass (top);

system («pause»);

return 0;

}

Результатом работы программы выше будет вывод чисел в порядке их возрастания от 1 до 10.

3 Классы

Классы предоставляют программисту возможность моделировать объекты, которые имеют атрибуты (внутренние данные) и варианты поведения или операции (внутренние функции). Типы, содержащие данные-элементы и функции-элементы, обычно определяются в C++ с помощью ключевого слова class.

Функции-элементы иногда в других объектно-ориентированных языках называют методами, они вызываются в ответ на сообщения, посылаемые объекту. Сообщение соответствует вызову функции-элемента.

Определение класса

Определение класса начинается с ключевого слова class. Тело определения класса заключается в фигурные скобки ({}). Определение класса заканчивается точкой с запятой.

{

public:

List (int=0, int=0);

void vvod (int);

void poisk ();

private:

int number;

char name [10];

char surname [10];

int age;

};

Когда класс определен, его можно использовать в качестве типа в объявлениях, например, следующим образом:

List man // объекта класса List;

List anketa [5], // массив объектов класса List;

List *ListPtr // указатель на объект класса List.

Имя класса становится новым спецификатором типа. Может существовать множество объектов класса как и множество переменных типа, например, такого, как int. Программист по мере необходимости может создавать новые типы классов.

Спецификаторы доступа

Метки public: (открытая) и private: (закрытая) называются спецификаторами доступа к элементам. Любые данные-элементы и функции- элементы, объявленные после спецификатора доступа к элементам public: (и до следующего спецификатора доступа к элементам), доступны при любом обращении программы к объекту класса. Любые данные-элементы и функции-элементы, объявленные после спецификатора доступа к элементам private: (и до следующего спецификатора доступа к элементам), доступны только функциям-элементам этого класса. Спецификаторы доступа к элементам всегда заканчиваются двоеточием (:) и могут появляться в определении класса много раз и в любом порядке.

По умолчанию режим доступа для классов — private (закрытый), так что все элементы после заголовка класса и до первого спецификатора доступа являются закрытыми.

Основная задача открытых элементов состоит в том, чтобы дать клиентам класса представление о возможностях, которые обеспечивает класс. Эти возможности описываются открытым интерфейсом класса. Клиентов класса не должно касаться, каким образом класс выполняет их задачи. Закрытые элементы класса и описания открытых функций-элементов недоступны для клиентов класса. Эти компоненты составляют реализацию (implementation) класса.

Из того, что данные класса закрытые, не следует, что клиенты не могут изменять эти данные. Данные могут быть изменены функциями-элементами или друзьями этого класса.

Конструкторы и деструкторы

В классе имеется функцию-элемент с тем же именем, что и класс List (int=0, int=0);. Она называется конструктором этого класса. Конструктор — это специальная функция-элемент, которая инициализирует данные-элементы объекта этого класса. Конструктор класса вызывается автоматически при создании объекта этого класса. Конструктор List просто присваивает начальные значения, равные 0, данным-элементам, (number=0; age=0;). Это гарантирует, что объект при его создании находится в известном состоянии. Неправильные значения не могут храниться в данных-элементах объекта типа List, поскольку конструктор автоматически вызывается при создании объекта типа List, а все последующие попытки изменить данные-элементы тщательно рассматриваются функцией setList.

Функция с тем же именем, что и класс, но со стоящим перед ней символом тильда (~), называется деструктором этого класса. Деструктор производит «завершающие служебные действия» над каждым объектом класса перед тем, как память, отведенная под этот объект, будет повторно использована системой.

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

Деструктор не принимает никаких параметров и не возвращает никаких значений. Класс может иметь только один деструктор — перегрузка деструктора не разрешается.

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

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

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

Конструкторы статических локальных объектов вызываются сразу же, как только процесс выполнения достигает места, где объекты были впервые объявлены. Соответствующие деструкторы вызываются, когда завершается main или когда вызывается функция exit.

Описание функций.

После спецификатора доступа к элементам private следуют два целых элемента (number и age), а также два массива символов (name и surname). Это говорит о том, что эти данные-элементы класса являются доступными только функциям-элементам класса. Таким образом, данные-элементы могут быть доступны только тем функциям, прототипы которых включены в определение этого класса. Обычно данные — элементы перечисляются в части private, а функции-элементы — в части public.

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

После того, как класс определен, и его функции-элементы объявлены, эти функции-элементы должны быть описаны. Каждая функция-элемент может быть описана прямо в теле класса (вместо прототипа функции класса) или после тела класса. Когда функция-элемент описывается после соответствующего определения класса, имя функции предваряется именем класса и бинарной операцией разрешения области действия:: (void List::vvod (int a)). Поскольку разные классы могут иметь элементы с одинаковыми именами, операция разрешения области действия «привязывает» имя элемента к имени класса, чтобы однозначно идентифицировать функции-элементы данного класса.

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

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

Отделение интерфейса от реализации

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

При отделении реализации от интерфейса программа разбивается на ряд файлов. При построении программы на C++ каждое определение класса обычно помещается в заголовочный файл, а определения функций-элементов этого класса помещаются в файлы исходных кодов с теми же базовыми именами. Заголовочные файлы включаются (посредством #include) в каждый файл, в котором используется класс, а файлы с исходными кодами компилируются и компонуются с файлом, содержащим главную программу.

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

Директивы препроцессора предотвращают включение кода между #ifndef и #endif, если определено имя LIST_Н. Если заголовок еще не включался в файл, то имя LIST_Н определяется директивой #define и операторы заголовочного файла включаются в результирующий файл. Если же заголовок уже был включен ранее, LIST_Н уже определен и операторы заголовочного файла повторно не включается.

Программа-пример использует класс List. Эта программа создает массив anketa состоящий из 5 объектов класса List. Когда объект создается, автоматически вызывается конструктор List.

Затем заполняются анкетные данные для 5 человек. После этого проводится поиск в анкетах с целью выявить людей, возраст которых более 18 лет, но не превышает 27. Анкетные данные найденных субъектов выводятся на экран.

// Заголовочный файл LIST. H Объявление класса List. Функции-элементы определены в LIST. СРР

// Предотвращение многократного включения заголовочного файла

#ifndef LIST_H

#define LIST_H

class List {

public:

List (int=0, int=0);

~List ();

void vvod (int);

void poisk ();

private:

int number;

char name [10];

char surname [10];

int age;

};

#endif

// Исходный файл List. cpp содержащий описания функций.

// Определения функций-элементов для класса List.

#include <iostream>

#include"list. h»

using namespace std;

List::List (int number; int age;) {number=0; age=0;}

void List::vvod (int a)

{

number=a;

cout <<«№ "<<number <<endl;

cout <<«Ввести имя -»;

cin>> name;

cout <<«Ввести фамилию -»;

cin>> surname;

cout <<«Ввести возраст -»;

cin>> age;

cout <<endl;

}

void List::poisk ()

{

if ((age> =18) && (age <=27)) {

cout <<«№ " <<number <<endl;

cout <<«Имя — " <<name <<endl;

cout <<«Фамилия — " <<surname <<endl;

cout <<«Возраст " <<age <<endl <<endl;

}

}

// Исходный файл main. cpp

#include <iostream>

#include"list. h»

using namespace std;

main ()

{

List anketa [5]; int i;

for (i=0; i <5; i++)

anketa [i].vvod (i+1);

for (i=0; i <5; i++)

anketa [i].poisk ();

system («Pause»);

return 0;

}

3.1 Создание дружественных функций

Бесплатный фрагмент закончился.

Купите книгу, чтобы продолжить чтение.