
- •Часть 1
- •Общие сведения Сведения об эумк
- •Методические рекомендации по изучению дисциплины
- •Рабочая учебная программа
- •Учреждение образования
- •«Белорусский государственный университет
- •Информатики и радиоэлектроники»
- •Часть 2 __184__
- •Содержание дисциплины
- •1. Индивидуальные практические занятия, их характеристика
- •2. Контрольные работы, их характеристика
- •3. Курсовой проект, его характеристика
- •4. Литература
- •4.1. Основная
- •4.2. Дополнительная
- •5. Перечень компьютерных программ, наглядных и других пособий, методических указаний и материалов и технических средств обучения
- •Протокол согласования учЕбной программы по изучаемой учебной дисциплине с другими дисциплинами специальности
- •Теоретический раздел Введение
- •1. Основные типы данных
- •1.1. Общие сведения
- •1.2. Данные типа int
- •1.3. Данные типа char
- •1.4. Модификаторы доступа const и volatile
- •1.5. Данные вещественного типа (с плавающей точкой)
- •1.6. Элементарный ввод-вывод
- •1.7. Структура простой программы на языке Си
- •2. Операции и выражения
- •2.1. Выражение и его интерпретация
- •2.2. Основные операции
- •2.2.1. Арифметические операции
- •2.2.2. Побитовые логические операции
- •2.2.3. Операции сдвига
- •2.2.4. Операция присваивания
- •2.2.5. Операция sizeof
- •2.2.6. Преобразование типов в выражениях
- •2.2.7. Операция преобразования типов
- •2.2.8. Приоритеты в языке Си
- •3. Операторы управления вычислительным процессом
- •3.1. Оператор if
- •3.2. Операции отношения
- •3.3. Логические операции
- •3.4. Операция запятая
- •3.5. Операция условия ?:
- •3.6. Оператор безусловного перехода goto
- •3.7. Оператор switch
- •`` ` `3.8. Операторы цикла
- •3.8.1. Оператор for
- •3.8.2. Оператор while
- •3.8.3. Оператор do...While
- •3.9. Оператор break
- •3.10. Оператор continue
- •4. Массивы и указатели
- •4.1. Одномерные массивы и их инициализация
- •4.2. Многомерные массивы и их инициализация
- •4.3. Объявление указателей
- •4.4. Операции над указателями
- •1) Взятие адреса
- •2) Косвенная адресация или разыменование указателя
- •3) Увеличение или уменьшение значения указателя на целое число
- •4) Разность указателей
- •5) Сравнение указателей
- •6) Присваивание указателей друг другу
- •4.6. Связь между указателями и массивами
- •4.7. Динамическое распределение памяти
- •4.8. Массивы указателей
- •5. Функции
- •5.1. Общие сведения
- •5.2. Область видимости переменных
- •5.2.1. Локальные переменные
- •5.2.2. Глобальные переменные
- •5.3. Передача параметров в функцию
- •5.4. Рекурсивные функции
- •5.5. Использование функций в качестве параметров функций
- •5.6. Указатели на функции
- •5.7. Структура программы на Си
- •5.8. Передача параметров в функцию main()
- •6. Строки
- •7. Классы хранения и видимость переменных
- •7.1. Общие сведения
- •7.2. Автоматический класс хранения (auto)
- •7.3. Регистровый класс хранения (register)
- •7.4. Статический класс хранения (static)
- •7.5. Внешний класс хранения (extern)
- •7.6. Заключение
- •8. Структуры, объединения и перечисления
- •8.1. Общие сведения
- •8.2. Инициализация структурных переменных
- •8.3. Вложенные структуры
- •8.4. Указатели на структуры
- •8.5. Массивы структурных переменных
- •8.6. Передача функциям структурных переменных
- •8.7. Оператор typedef
- •8.8. Поля битов в структурах
- •8.9. Объединения
- •8.10. Перечисления
- •9. Динамические структуры данных
- •9.1. Общие сведения
- •9.2. Связные списки
- •9.2.1. Односвязные списки
- •9.2.2. Двусвязные списки
- •9.2.3. Циклические списки
- •9.3. Стеки
- •9.4. Очереди
- •9.5. Деревья
- •9.5.1. Понятие графа
- •9.5.2. Бинарные деревья
- •10. Файлы
- •10.1. Общие сведения
- •10.2. Открытие и закрытие файлов
- •10.3. Функции ввода-вывода для работы с текстовыми файлами
- •10.4. Произвольный доступ к файлу
- •10.5. Функции ввода-вывода для работы с бинарными файлами
- •11. Директивы препроцессора
- •11.1. Основные понятия
- •11.2. Директива #include
- •11.3. Директивы препроцессора #define и #undef
- •11.3.1. Символические константы
- •11.3.2. Макросы с параметрами
- •11.3.3. Директива #undef
- •11.4. Условная компиляция
- •11.5. Директивы # и ##
- •12. Модульное программирование
- •13. Введение в объектно-ориентированное программирование
- •13.1. Постановка задачи
- •13.2. Решение задачи средствами Си
- •13.5. Наследование
- •13.6. Перегрузка
- •13.7. Ссылочный тип
- •Литература
- •Приложение 1. Рекомендации по оформлению текстов программ
- •Тесты к теоретическому разделу Вопросы к разделу 1. Основные типы данных
- •Вопросы к разделу 2. Операции и выражения
- •Вопросы к разделу 3. Операторы управления вычислительным процессом
- •Вопросы к разделу 4. Массивы и указатели
- •Вопросы к разделу 5. Функции
- •Вопросы к разделу 6. Строки
- •Вопросы к разделу 7. Классы хранения и видимость переменных
- •Вопросы к разделу 8. Структуры, объединения и перечисления
- •Вопросы к разделу 9. Динамические структуры данных
- •Вопросы к разделу 10. Файлы
- •Вопросы к разделу 11. Директивы препроцессора
- •Вопросы к разделу 12. Модульное программирование
- •Вопросы к разделу 13. Введение в ооп
- •Правильные ответы на вопросы тестов к теоретическому разделу
- •Вопросы к теоретическому зачету
- •Варианты индивидуальных заданий
- •Контрольная работа №2
- •Варианты индивидуальных заданий
- •Индивидуальные практические работы Указания к выбору варианта индивидуальных практических работ
- •Индивидуальная практическая работа № 1. Массивы и строки
- •Варианты индивидуальных заданий
- •Индивидуальная практическая работа № 2. Динамические структуры данных
- •Варианты индивидуальных заданий
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) в таком порядке:
Подключение заголовочных файлов библиотек
Определение типов данных «элемент списка» и «список»
Определение функций работающих с элементами списка
Определение функций работающих со списком
Класс 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.