Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ЯП - ПОИТ (Бахтизин) часть 1 редакт.doc
Скачиваний:
0
Добавлен:
01.04.2025
Размер:
1.76 Mб
Скачать

13.2. Решение задачи средствами Си

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

Пример 13.3. Интерфейсная часть стека.

struct TStack;

struct TStack *create_stackt();

void delete_stack(struct TStack **stack);

void push(struct TStack *stack, int a);

int pop(struct TStack *stack);

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

Чтобы окончательно скрыть реализацию, разобьём проект на файлы: выделим для реализации стека файл stack.c и перенесем туда определения всех функций из примера 13.2. Основной файл проекта, (содержащий функцию main()) назовем main.c. Теперь осталось лишь создать заголовочный файл, содержащий объявления функций из примера 13.3, и включить его в stack.c и main.c.

Сейчас можно говорить о том, что первый недостаток (в некоторой степени) устранен. Стек разбит на две части: интерфейс содержит все то, что объявлено в stack.h, реализация, все то, что определено в stack.c.

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

13.3. C++. Классы

Языки, поддерживающие концепцию объектно-ориентированного программирования (ООП) вводят средства, позволяющие решать вышеназванные проблемы. Одним из таких языков является C++. Основное средство, реализованное в C++ для поддержки парадигмы ООП – класс.

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

Пример 13.4. Класс iStack

// Класс iStack (стек чисел_типа_int)

class iStack

{

public:

iStack(); // Конструктор

~iStack(); // Деструктор

void push(int a);

int pop();

private:

struct TList *top; // Указатель на вершину стека

};

В первых строках содержится заголовок класса, здесь указано, что определяемая структура – класс, а идентификатор, который станет именем нового типа, – iStack. Ключевое слово public говорит компилятору о том, что последующие объявления будут относиться к интерфейсной части класса. Далее идут объявления следующих функций: конструктор и деструктор, push() и pop(). Именно эти четыре функции были вынесены в интерфейсную часть стека (пример 13.3).

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

Здесь и далее, для простоты, подразумевается, что в точке объявления класса доступны определенные ранее «элемент списка» и «список». Преобразовав таким образом стек из примера 13.2, поместим код в один файл (например, stack.cpp) в таком порядке:

  1. Подключение заголовочных файлов библиотек

  2. Определение типов данных «элемент списка» и «список»

  3. Определение функций работающих с элементами списка

  4. Определение функций работающих со списком

  5. Класс iStack

На следующем шаге необходимо определить функции, объявленные внутри класса. Этот код следует добавить в конец файла stack.cpp:

iStack::iStack() { top = create_list(); }

void iStack::~iStack() { delete_list(&top); }

void iStack::push(int a) { add_item(top, a); }

int iStack::pop() { return remove_item(top); }

В части кода, использующей объекты, изменились имена функций инициализации и деинициализации. Их пришлось переименовать так, чтобы компилятор мог определить, какая именно из функций объявленных в классе конструктор, а какая деструктор (для автоматического их вызова). В C++ конструктор и деструктор должны называться так же, как и класс, а деструктор, кроме того, должен иметь тильду “~” перед идентификатором (как в примере 13.3).

Кроме того, перед каждым именем функции обязательно ставить имя класса и оператор разрешения области видимости “::”. Таким образом, функция, ранее объявленная, как int pop(), теперь будет объявлена так: int iStack::pop(), и т.д.

Работать со стеком теперь тоже надо по-другому. Для того чтобы создать стек не надо явно вызывать некоторую функцию – достаточно лишь объявить переменную типа iStack (т.е. создать объект класса iStack). В момент, когда в своем выполнении дойдет до оператора объявления конструктор будет вызван автоматически. Для того, чтобы выполнить над стеком некоторое действие, нужно для объекта “стек” вызвать соответствующий метод (так иногда называют функцию-член класса). Уничтожение объекта также автоматизировано. Оно, как и уничтожение любой встроенной переменной, произойдет при выходе из объемлющего блока. Проиллюстрируем все сказанное примером:

void main()

{

int i;

iStack stack;

for(i = 0; i <= 10; i++) stack.push(i*10);

for(i = 0; i <= 10; i++) printf("%d\n", stack.pop());

getch();

}

В четвёртой строке объявлена переменная типа iStack. В отличие от Си, в C++ это объявление является оператором, вызывающим конструктор класса iStack для объекта stack.

Следует упомянуть, что, поскольку эта переменная имеет автоматический тип хранения, она будет размещена в системном стеке. Размер памяти, необходимой для ее размещения, определяется суммарным размером членов-данных класса iStack. Может создаться ошибочное впечатление, что в области памяти, отведенной под объект, помещается все, что объявлено внутри класса. Однако это не верно: функции, объявленные в классе хранятся вне какого-либо блока данных.

Далее, в циклах, для объекта stack вызываются функции члены push() и pop(). Как видно из примера, доступ к ним, как и к элементам структуры, осуществляется посредством оператора “.” (точка):

stack.push(i*10);

stack.pop();

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

push(stack, i*10);

pop(stack);

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

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

delete_stack(&stack);

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

13.4. C++ и объектно-ориентированное программирование

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

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

Так, можно говорить о концепции вещественного числа (с операциями +, -, *, /, …), концепции человека (с операциями «вывести на экран свою фамилию», «начислить себе зарплату», «идти выносить мусор»), стека (со сложной структурой данных и простым интерфейсом «push» и «pop»).

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

определение_класса ::=

“class | struct” идентификатор “{” { блок_объявлений } “};”

блок_объявлений ::=

дисциплина_доступа { объявление_члена_класса }

дисциплина_доступа ::=

“private:” | “public:”

объявление_члена_класса ::=

объявление_функции | объявление_переменной

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

Очевидно, дисциплина доступа к члену класса определяется ближайшим (сверху) квалификатором доступа. В случае, когда такого квалификатора нет, член получает права доступа по умолчанию: если описание класса началось ключевым словом class – private, если struct – public.

Важно помнить, что понятия объявления и определения (declaration and definition) различаются. Так, например, объявить функцию – значит задать ее прототип:

int summ(int, int);

определить функцию, значит привести ее тело:

int summ(int a, int b) { return a+b; };

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

class some_class; // Объявление класса

class some_class // Определение класса

{

//...

};

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

Относительно данного класса весь код программы можно условно разделить на две части: код, который «принадлежит» классу, и весь остальной код. К первой группе относятся, в частности, определения функций-членов. К закрытым членам класса (т.е. к функциям и переменным, объявленным с дисциплиной доступа private) можно обратиться только из тех частей программы, которые принадлежат классу. Рассмотрим определение функции извлечения элемента из стека:

int iStack::pop()

{

return remove_item(top);

}

Для того чтобы определить функцию-член класса необходимо в ее заголовке, непосредственно перед именем (через два двоеточия “::”) указать название класса. В нашем примере, функции remove_item, в качестве параметра, передается закрытый член класса iStack – переменная top. Такое обращение к переменной правомерно, т.к. происходит в определении функции-члена того же класса. Точно так же, при необходимости, из функции, принадлежащей области видимости класса, можно обращаться к его открытым членам и к членам-функциям.

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

Для класса могут быть объявлены две функции специального вида. Первая из них называется конструктор (constructor). Он не имеет типа возвращаемого значения и его имя должно совпадать с именем класса. Гарантируется, что если у класса есть конструктор, то он будет вызван для каждого объекта класса перед первым его (объекта) использованием.

Второй функцией специального назначения является деструктор (destructor). Его имя также должно совпадать с именем класса и в объявлении не надо указывать тип возвращаемого значения, но начинаться оно должно с тильды “~”. Гарантируется, что если у класса есть деструктор, то он будет вызван перед выходом из блока объемлющего объявление объекта данного класса:

class Group

{

public:

Group(int); // Constructor

~Group(); // Destructor

// ...

private:

int *marks;

// ... Другие члены.

};

Group::Group(int number)

{

// Выделение памяти для массива оценок

// студентов группы из number целых чисел

marks = (int *) malloc(number);

// ... Инициализация других переменных.

}

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

В примере конструктору должен передаваться параметр – величина массива. Ниже приведен пример функции создающей объект класса Group, и передающий, в качестве параметра, его конструктору число 15:

void function(void)

{

Group g251002(15);

// ... Использование объекта g251002

}

Из того, как вызывается конструктор, следует, что он (конструктор) не может иметь возвращаемого параметра. Это правило распространяется и на деструктор:

Group::~Group()

{

// Удаление массива из динамической памяти

free(marks);

}

Обычно, деструктор освобождает ресурсы, выделенные для объекта конструктором.

Как и к членам структуры Си, доступ к членам класса производится посредством операторов “.” (точка) и “->” (стрелка). Таким образом, можно получить доступ не только к данным объекта, но и к его функциям:

g251002.~Group();

g251002.Group(25);

Таким образом, для объекта g251002 был вызван деструктор, который вернул в кучу память, выделенную для массива из 15 элементов, а затем конструктор, который создал новый массив из 25 элементов.

Если имеется указатель на объект, то те же действия можно выполнить таким образом:

Group *pg = &g251002;

pg->~Group();

pg->Group(25);

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