
- •Часть 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. Динамические структуры данных
- •Варианты индивидуальных заданий
11.5. Директивы # и ##
Ранее нами уже были рассмотрены макросы и способы их применения в программах на языке Си. Для расширения возможностей макросов существуют ещё две директивы: # и ##.
Оператор # превращает аргумент, которому он предшествует, в строку, заключенную в кавычки. Например:
#define mkstr(s) # s
Данный оператор преобразует аргумент в строку. Вызов mkstr(123) раскроется в ("123").
Оператор ## используется для конкатенации двух лексем. Например:
#define concat(a,b) a ## b
Данный оператор соединяет ("склеивает") свои аргументы, может использоваться для формирования идентификаторов. Вызов concat(x,4) раскроется в (x4).
Допускается ставить по одному необязательному пробелу слева и справа от знака ##. При раскрытии макроопределения препроцессор удалит пробелы. Вложенные маровызовы, расположенные в строке макроопределения, раскрываются только в момент раскрытия самого макроса, а не в момент его определения.
12. Модульное программирование
В общем случае, программа на Си может состоять из нескольких отдельно компилируемых модулей. Каждый из них находится в своем текстовом файле, а после компиляции, «превращается» в т.н. объектный файл с расширением .obj. Вот, например, как может выглядеть программа печати строки, если ее исходный код разбить на два модуля:
Module1.c |
|
Module2.c |
int main() { myprint(“Joe”); } |
|
void myprint(char* s) { while (*s) putch(s++); } |
Оба файла удовлетворяют всем требованиям языка, т.к. опережающее определение функций не является обязательным в Си. Однако, несмотря на то, что обе программы будут скомпилированы, запустить любую из них на выполнение не удастся, более того, не будет создан исполнимый файл, но объектные файлы и в том и в другом случае созданы будут. Более того, если их передать линковщику, будет создана программа, выводящая на экран строку.
Несомненно, двухэтапный подход к созданию выполнимого файла открывает множество возможностей, таких, например, как создание библиотек. Технически, связывание отдельно скомпилированных модулей, возможно благодаря тому, что в объектные файлы кроме машинных кодов команд и данных программы заносятся т.н. неразрешенные символьные ссылки. В нашем случае объектный файл Module1.obj содержал вместо конкретного указателя имя функции myprint(), а модуль Module2.obj информацию для линковщика о том, что в Module2.obj есть функция myprint(). Линковщику оставалось лишь подставить вместо неразрешенной ссылки из первого модуля адрес функции из второго.
Однако у такого подхода есть и обратная сторона: тот факт, что перед вызовом функции не обязательно иметь её прототип, делает невозможной проверку типов во время компиляции. Так, функция myprint может быть вызвана из модуля Module1.c (не содержащего ее прототипа) с параметрами не только неверного типа, но и количества. Очень остро эта проблема встает в программах, с большим количеством функций, особенно ввиду того, что, обычно, чем больше проект, тем активней в нем используется модульное программирование.
Однако способ устранения данного недостатка модульного программирования лежит на поверхности, и заключается в использовании прототипов (заголовков). Если бы текст Module1.c выглядел так:
Module1.c |
void myprint(char *s)
int main() { myprint(“Joe”); } |
никаких проблем с неверными параметрами функции возникнуть бы не могло. Встретив прототип, компилятор не допустит неверного преобразования типов. Но это еще не способ решения проблемы: во-первых, переписывая из раза в раз заголовок можно и в нем допустить ошибку, а во-вторых, такой подход просто не удобен, и значит, программист просто поленится переписать заголовки всех функций, которые собирается использовать. Перечислять недостатки можно было бы и дальше, но намного проще устранить их, создав текстовый файл, в котором будут храниться заголовки всех функций данного модуля. Читатель, наверное, уже догадался, что, раз файл должен хранить заголовки функций, его нужно называть заголовочным, и дать ему расширение “h” (от английского слова header – заголовок). В нашем случае заголовочный файл будет иметь имя Module2.h, что вполне логично, т.к. содержать он будет прототипы функций из Module2.c:
Module2.h |
|
Module2.c |
void myprint(char *s); |
|
#include “Module2.h”
void myprint(char* s) { while (*s) putch(s++); } |
Теперь, добавив соответствующую строку в module1.c, получим намного более удобную и надежную конструкцию, чем та, что была получена вначале.
Module1.c |
// Вместо этой строки в файл будет подставлено содержимое // заголовочного файла, т.е. прототипы функций из Module2.c #include “Module2.h” int main() { myprint(“Joe”); } |
Заметьте, что заголовочный файл включен в Module2.c не случайно: это не даст кодировщику забыть изменить прототипы, если он изменит заголовок одной из функций модуля. Так, если
void myprint (char* s) {}
по каким-то причинам, будет заменен, на
char* myprint (char* s, int *res)
а заголовок останется прежним, программист увидит сообщение компилятора о том, что прототип из Module2.h и заголовок функции в Module2.c не совпадают.
Обычно IDE предоставляет удобный механизм создания многомодульных программ так, в Borland C++ 3.1 это можно сделать, выбрав в меню пункт project -> open project и введя несуществующее в данном каталоге имя. При этом будут созданы файлы имя.prj и имя.dsk, в которых IDE сохранит служебную информацию о проекте. Далее, в служебном окне project, можно, нажав insert, ввести имена файлов, которые будут содержать исходный код (существующие, или несуществующие). Подробнее о работе с проектами в вашей среде разработки можно прочесть в файле справки, или в документации по системе.
Не следует забывать о том, что IDE не различают расширения файлов, поэтому программист сам должен следить за тем, чтобы логическая структура модулей программы не была нарушена. Например, исходный код некоторых функций может быть добавлен в заголовочные файлы, или включен в файлы, содержащие исходный код и компилятор не сочтет это ошибкой, однако так поступать не стоит. Вот самый типичный пример неправильного стиля программирования:
Program.c |
|
Modl.h |
#include “Modl.h” int main() { myprint(“Joe”); } |
|
void myprint(char *s) { while (*s) putch(s++); } |
Здесь, во-первых, в файле с расширением h одна из функций содержит тело, а во-вторых, один файл, содержащий определения функций, включен директивой include, в другой.
Чтобы проверить работоспособность примера программы из нескольких модулей, создадим проект с именем test, а Module1.c, Module2.c и Module2.h переименуем в main.c, unit.c и unit.h соответственно. Если сохранить все файлы в одном каталоге, должна получиться примерно такая картина:
P:\TC\C\Projects>dir
Содержимое папки P:\TC\C\Projects
22.02.2003 19:45 <DIR> .
22.02.2003 19:45 <DIR> ..
22.02.2003 19:43 180 Main.c
22.02.2003 19:45 27 533 Test.dsk
22.02.2003 19:45 4 143 Test.prj
22.02.2003 19:43 82 Unit.c
22.02.2003 19:43 23 Unit.h
Теперь, если открыт проект (при этом ни одно окно с кодом может быть не открыто) при запуске процесса компиляции будет создан исполнимый файл. Процесс компиляции проходит следующим образом: вначале, по отдельности, компилируются все модули (файлы, включенные в проект), затем, полученные объектные файлы передаются линковщику, который разрешает неразрешенные ссылки и создает исполнимый файл. Следует упомянуть, что компиляция каждого модуля проходит вне зависимости от остальных – так, как если бы был открыт не проект, а лишь этот файл. Естественно, если на этапе компиляции любого из модулей будут найдены ошибки, или, при попытке создания выполнимого файла, линковщик обнаружит, что ни в одном из модулей нет тела функции, вызванной где-либо, IDE сообщит об ошибке.
Довольно часто встречается ситуация, когда один заголовочный файл содержит другой. Ниже будут рассмотрены случаи, когда такое решение оправдано и даже необходимо. Сейчас, следует лишь заметить, что заголовочный файл может быть включен в некоторый модуль несколько раз:
Program.c |
|
Modl.h |
#include “Modl.h #include “Another.h” int main() { myprint(“Joe”); } |
|
void myprint(char *s); |
В данном примере таким заголовочным файлом является Another.h – он необходим как в Modl.h, так и в Program.c. С одной стороны, если Modl.h построен правильно и не содержит определений, это не грозит никакими ошибками, т.к. непротиворечивые объявления, могут встретиться в модуле несколько раз. С другой стороны, многократное включение одного и того же файла, ведет к неоправданным потерям времени при компиляции.
Данная сложность легко разрешима при помощи директив условной компиляции. Достаточно лишь оформить заголовочный файл следующим образом:
Another.h |
#ifndef ANOTHERH #define ANOTHERH //------------------------------------- int *someheader(int, int*, char); void oneheader(char*); const int *twoheader(int, char); // ... //------------------------------------- #endif |
Компилятор заходит в блок #ifndef, если символическая константа ANOTHERH еще не объявлена, и, первым делом, объявляет ее. Теперь, сколько бы раз не встретился заголовочный файл, работа по включению его содержимого в модуль будет произведена лишь однажды.
Сегодня модульное программирование применяется очень широко. Это обусловлено множеством различных факторов, важнейшими из которых являются:
Необходимость совместной работы множества программистов.
Возможность разбить исходный код на несколько частей.
Удобство создания библиотек общеиспользуемых функций.
Вопрос о том, как именно распределить код по модулям и какие объявления поместить в заголовочных файлах вовсе не является тривиальным. Однако есть несколько типичных ситуаций, который встречаются довольно часто, самой простой из которых, является создание библиотечного модуля. Фактически она уже была рассмотрена выше: для файла с исходным кодом, создается заголовочный файл, который должен быть включен во все модули, использующие определенные в нем функции. В дальнейшем, будем называть эти файлы lib.c и lib.h.
Очень часто, для сокрытия исходного кода распространяемых на коммерческой основе библиотек, lib.c компилируют и распространяют вместо него lib.obj и, естественно, lib.h.
Следует помнить о том, что модуль может содержать не только функции, но и переменные, доступ к которым может быть получен извне. Однако, т.к. тип переменной компилятор должен «знать» до первого ее использования опустить объявление переменной нельзя:
Lib.h |
|
Lib.c |
#ifndef LIBH #define LIBH //----------------------- // Заголовочный файл не // содержит объявления // useful_var //----------------------- #endif |
|
#include “Lib.h” int useful_var; |
Однако объявить в заголовочном файле переменную не так просто, ведь при включении заголовочного файла в программу, будет создана локальная копия переменной:
Lib.h |
|
Program.c |
#ifndef LIBH #define LIBH //----------------------- #include “Lib.h” // Объявление? int useful_var; //----------------------- #endif |
|
#include “Lib.h void someFunction(void) { useful_var = 0; } |
В приведенном примере включение в program.c файла lib.h равносильно включению строки “int useful_var;”:
Program.c |
int useful_var; // Локальная копия переменной void somefunction(void) { useful_var = 0; } |
В памяти будет создана еще одна переменная, к которой и будет обращаться somefunction из примера. Обойти эту проблему можно при помощи ключевого слова extern:
Lib.h |
#ifndef LIBH #define LIBH //------------------------------------- #include “Lib.h” // Объявление! extern int useful_var; //------------------------------------- #endif |
Теперь somefunction изменит именно переменную из модуля lib.h. Следует помнить, что объявление, содержащее инициализацию, становится определением:
extern int useful_var = 0; // Определение!
Ключевое слово extern можно использовать и с объявлениями функций, хотя и является там избыточным:
extern void somefunction(void);
Библиотечный модуль может содержать функции и переменные, предназначенные лишь для внутреннего использования, их прототипы не следует включать в заголовочные файлы. Более сложной и интересной является ситуация, когда разные функции должны быть доступны из разных модулей. Пусть, например, lib.c содержит три функции:
Lib.c |
void f1(void); // Для общего пользования void f2(void); // Для пользования в специальных модулях void f3(void); // Для внутреннего пользования |
В данной ситуации разумно создать несколько заголовочных файлов, каждый из которых будет включать прототипы тех функций, которые должны быть доступны из модулей соответствующего типа. В нашем случае необходимо создать два таких файла:
PublicLib.h |
|
ProtectedLib.h |
#ifndef PUBLICLIB #define PUBLICLIB //---------------------- void f1(void); //---------------------- #endif |
|
#ifndef PROTECTEDLIB #define PROTECTEDLIB //----------------------- void f1(void); void f2(void); //----------------------- #endif |
Не следует забывать и о том, что сам библиотечный модуль должен включать оба заголовочных файла:
Lib.c |
#include “PublicLib.h” #include “ProtectedLib.h” void f1(void); void f2(void); void f3(void); |
Возможна и обратная ситуация, когда один заголовочный файл включает прототипы функций из нескольких модулей. Обычно в этом появляется необходимость, когда программисту удобнее работать с несколькими файлами небольшого размера, чем с одним огромным, но содержание их таково, что разбивать функции на логические группы смысла не имеет.
Большой интерес также представляет вопрос о том, куда включать заголовочные файлы, если они нужны в модуле, который сам имеет заголовочный файл. Например, если в lib.c используются функции из стандартной библиотеки conio.h, то куда поместить строку
#include <ConIO.h>
в lib.c или в lib.h? Ответ не однозначен, но обычно, директива помещается в заголовочный файл только в том случае, если без этого не обойтись:
Lib.h |
//----------------------------------------------------------- #include <ConIO.h> /* Структура text_info объявлена в ConIO.h */ somefunction(struct text_info* sti); /* Перечислимый тип text_modes также объявлен в ConIO.h */ enother_function(text_modes tm); //----------------------------------------------------------- |
Следует заметить, что т.к. lib.h включен в lib.c подключать сторонние библиотеки в обоих файлах не стоит:
Lib.h |
|
Program.c |
//---------------------- #include <ConIO.h> //---------------------- |
|
#include “Lib.h” // Нежелательно... #include <ConIO.h> // ... |
Если же можно обойтись без подключения библиотеки в заголовочном файле, то так и следует поступить. Это позволит не включать в модули, прямо не использующие библиотеку, ее заголовочный файл.