Лабораторная работа № 10 / 10_проекты_a5.doc
Лабораторная работа № 10
Многофайловые программы.
Работа с проектом
Цель работы: 1) изучить правила описания раздельно откомпили-рованных функций; 2) приобрести навыки использования проектов при написании программ на языке C++.
Теоретические сведения
Все нетривиальные программы собираются из нескольких раздельно компилируемых единиц (их принято называть просто файлами). В этой лабораторной работе описано, как раздельно откомпилированные функции могут обращаться друг к другу, как такие функции могут совместно пользоваться данными (разделять данные), и как можно обеспечить согласованность типов, которые используются в разных файлах программы.
Иметь всю программу в одном файле обычно невозможно, поскольку коды стандартных библиотек и операционной системы находятся где-то в другом месте. Кроме того, хранить весь текст пользовательской программы в одном файле, как правило, непрактично и неудобно. Способ организации программы в файлы может помочь читающему охватить всю структуру программы, а также может дать возможность компилятору реализовать эту структуру. Поскольку единицей компиляции является файл, то во всех случаях, когда в файл вносится изменение (сколь бы мало оно ни было), весь файл нужно компилировать заново. Даже для программы умеренных размеров время, затрачиваемое на перекомпиляцию, можно значительно снизить с помощью разбиения программы на файлы подходящих размеров.
Программа, состоящая из нескольких раздельно компилируемых файлов, должна быть согласованной в смысле использования имен и типов, точно так же, как и программа, состоящая из одного исходного файла. В принципе, это может обеспечить и компоновщик. Компоновщик - это программа, стыкующая отдельно скомпилирован-ные части вместе.
Программист может скомпенсировать недостаток поддержки со стороны компоновщика, предоставив дополнительную информацию о типах (описания). После этого согласованность программы обеспечивается проверкой согласованности описаний, которые находятся в отдельно компилируемых частях. Средства, которые это обеспечивают, в вашей системе будут. C++ разработан так, чтобы способствовать такой явной компоновке.
Компоновка
Если не указано иное, то имя, не являющееся локальным для функции или класса, в каждой части программы, компилируемой отдельно, должно относиться к одному и тому же типу, значению, функции или объекту. То есть, в программе может быть только один нелокальный тип, значение, функция или объект с этим именем. Рассмотрим, например, два файла:
// file1.c:
int a = 1;
int f() { /* что-то делает */ }
// file2.c:
extern int a;
int f();
void g() { a = f(); }
a и f(), используемые g() в файле file2.c,- те же, что определены в файле file1.c. Ключевое слово extern (внешнее) указывает, что описание a в file2.c является (только) описанием, а не определением. Если бы a инициализировалось, extern было бы просто проигнорировано, поскольку описание с инициализацией всегда является определением. Объект в программе должен определяться только один раз. Описываться он может много раз, но типы должны точно согласовываться. Например:
// file1.c:
int a = 1;
int b = 1;
extern int c;
// file2.c:
int a;
extern double b;
extern int c;
Здесь три ошибки: a определено дважды (int a; является определением, которое означает int a=0;), b описано дважды с разными типами, а c описано дважды, но не определено. Эти виды ошибок (ошибки компоновки) не могут быть обнаружены компилятором, который за один раз видит только один файл. Компоновщик, однако, их обнаруживает.
Следующая программа не является C++ программой (хотя C программой является):
// file1.c:
int a;
int f() { return a; }
// file2.c:
int a;
int g() { return f(); }
Во-первых, file2.c не C++, потому что f() не была описана, и поэтому компилятор сообщит об ошибке. Во-вторых, (когда файл file2.c фиксирован) программа не будет скомпонована, поскольку a определено дважды.
Имя можно сделать локальным в файле, описав его со спецификатором static. Например:
// file1.c:
static int a = 6;
static int f() { /* ... */ }
// file2.c:
static int a = 7;
static int f() { /* ... */ }
Поскольку каждое a и f описано как static, получающаяся в результате программа является правильной. В каждом файле своя переменная a и своя функция f().
Когда переменные и функции явно описаны как static, часть программы легче понять (вам не надо никуда больше заглядывать). Использование спецификатора static для функций может несколько улучшить характеристики программы, связанные с вызовом функции.
Рассмотрим два файла:
// file1.c:
const int a = 6;
inline int f() { /* ... */ }
struct s { int a,b; }
// file2.c:
const int a = 7;
inline int f() { /* ... */ }
struct s { int a,b; }
Поскольку правило "ровно одно определение" применяется к константам, inline-функциям и определениям функций так же, как оно применяется к функциям и переменным, то file1.c и file2.c не могут быть частями одной C++ программы. Но если это так, то как же два файла могут использовать одни и те же типы и константы? Коротко, ответ таков: типы, константы и т.п. могут определяться столько раз, сколько нужно, при условии, что они определяются одинаково. Полный ответ несколько более сложен (это объясняется в следующем разделе).
Заголовочные файлы
Типы во всех описаниях одного и того же объекта должны быть согласованными. Один из способов достижения этого, мог бы состоять в обеспечении средств проверки типов в компоновщике, но большинство компоновщиков - образца 1950-х, и их нельзя изменить по практическим соображениям. Другой подход состоит в согласовании исходного текста, передаваемого компилятору, или в наличии данных, позволяющих компилятору обнаружить несогласованности. Один несовершенный, но простой способ достичь согласованности состоит во включении заголовочных файлов, содержащих интерфейсную информацию, в исходные файлы, в которых содержится исполняемый код и/или определения данных.
Механизм включения с помощью #include - это чрезвычайно простое средство обработки текста для сборки фрагментов исходной программы в одну единицу (файл) для ее компиляции. Директива
#include "to_be_included"
замещает строку, в которой встретилось #include, содержимым файла "to_be_included". Его содержимым должен быть исходный текст на C++, поскольку дальше его будет читать компилятор. Включения обрабатывается отдельной программой, называемой C препроцессором, которую компилятор вызывает для преобразования исходного файла, поступившего от программиста, в файл без директив включения перед тем, как начать собственно компиляцию.
Для включения файлов из стандартной директории включения вместо кавычек используются угловые скобки. Например:
#include <myheader.h> // из стандартной директории включения
#include "myheader.h" // из текущей директории
Использование < > имеет то преимущество, что в программу фактическое имя директории включения не встраивается (как правило, сначала просматривается /usr/include/CC, а потом usr/include). К сожалению, пробелы внутри скобок в директиве include существенны:
#include < stream.h > // не найдет
Может показаться, что перекомпилировать файл заново каждый раз, когда он куда-либо включается, расточительно, но время компиляции такого файла обычно слабо отличается от времени, которое необходимо для чтения его некоторой заранее откомпилированной формы. Причина в том, что текст программы является довольно компактным представлением программы, и в том, что включаемые файлы обычно содержат только описания и не содержат программ, требующих от компилятора значительного анализа.
Следующее эмпирическое правило относительно того, что следует, а что не следует помещать в заголовочные файлы, является не требованием языка, а просто предложением по разумному использованию аппарата #include.
В заголовочном файле могут содержаться:
Определения типов | struct point { int x, y; } |
Описания функций | extern int strlen(const char*); |
Определения inline-функций | inline char get() { return *p++; } |
Описания данных | extern int a; |
Определения констант | const float pi = 3.141593 |
Перечисления | enum bool { false, true }; |
Директивы include | #include |
Определения макросов | #define Case break;case |
Комментарии | /* проверка на конец файла */ |
но никогда
Определения обычных функций | char get() { return *p++; } |
Определения данных | int a; |
Определения сложных констант-ных объектов | const tbl[] = { /* ... */ } |
В среде С принято, что заголовочные файлы имеют суффикс (расширение) .h. Файлы, содержащие определение данных или функций, должны иметь суффикс .c. Такие файлы часто называют, соответственно, ".h файлы" и ".c файлы".
Создание проекта
Для организации структуры программы используется такое понятие среды разработки, как проект. Проект в ИСР представляет собой единое пространство компоновки, обеспечивающее сборку программы из нескольких исходных файлов. При организации проекта в каждый файл проекта включаются соответствующие заголовочные файлы с описаниями используемых в данном файле функций, типов и константных выражений. Все файлы с определениями функций включаются в проект как раздельно компилируемые файлы и, после компиляции, создают единое пространство имён проекта.
При организации проекта в проект включаются все .cpp файлы, тогда как .h -файлы включаются непосредственно с использованием include.
Рассмотрим пример программы, описывающей домашнюю библиотеку из N книг (задаётся с клавиатуры), и выводящую на экран все книги нужного автора.
Разобьём программу на следующие модули:
Main.cpp - основная программа, интерфейс пользователя.
Const.h - заголовочный файл с описаниями типов и констант.
Func.h - заголовочный файл с объявлением функций.
Func.cpp заголовочный файл с описанием функций.
//main.cpp----------------------------------------------------------------------
#include <conio.h>
#include <iostream.h>
#include <stdlib.h>
#include <stdio.h>
#include ″func.h″
void main()
{ int N;
int K;
char auth[50];
cout<<”Input a book's quantity >”;
cin>>N;
cin.get();
book* bibl=InputBibl(N);
cout<<”Input an author name >”;
cin.get(auth,50);
cout<<”For author “<<auth<<” finding books:”<<endl;
K=printAuthor(bibl,N,auth);
cout<<”Counts - “<<K<<endl;
}
//const.h------------------------------------------------------------------------
struct book
{ char header[50];
char author[50];
};
//func.h-------------------------------------------------------------------------
#include ″const.h″
book* InputBibl(int N);
int printAuthor(book* B, int N, char auth[50]);
//func.cpp-----------------------------------------------------------------------
#include ″const.h″
#include <iostream.h>
#include <conio.h>
#include <stdio.h>
#include <string.h>
book* InputBibl(int N)
{ book* B=new book[N];
for(int i=0;i<N;i++)
{ cout<<”Input Header for “<<i<<” book >”;
cin.get(B[i].header,50);
cout<<”Input Author for “<<i<<” book >”;
cin.getline(B[i].author,50);
}
return B;
}
int printAuthor(book* B, int N, char auth[50])
{ int k=0;
for(int i=0;i<N;i++)
if(strcmp(B[i].author,auth)==0)
{ k++;
cout<<”Book - “<<k<<” - “<<B[i].header<<endl;
}
return k;
}
Примечание:
оператор new динамически выделяет память для массива и возвращает указатель на выделенную область. Принимает тип элемента и количество элементов, если количество не указано - выделяется память для одного элемента.
Создание группового проекта
Главное правило ведения группового проекта - чётко обозначенный интерфейс (заголовки всех используемых в модуле функций) модуля каждого программиста. При разработке на первом этапе оговаривается конечный интерфейс разрабатываемых модулей. Далее каждый из программистов разрабатывает свой модуль основываясь только на интерфейсе модулей других программистов. На заключительном этапе разработки модули объединяются и компилируется единая программа.
Пример задания:
Написать программу сортировки и поиска книги в магазине по автору или по названию.
Задание разбивается на следующие модули:
Модуль ввода/вывода (интерфейс пользователя) - основной модуль программы.
Интерфейс модуля (заголовки функций):
book* InputBibl(int N); //ввод книг в библиотеку
int printAuthor(book* B, int N, char auth[50]); //вывод на экран содержимого библиотеки
Модуль, содержащий функции сортировки.
Интерфейс модуля:
void SortABibl(book* B, int N); //сортировка книг по автору
void SortHBibl(book* B, int N); //сортировка книг по названию
Модуль, содержащий функции поиска;
Интерфейс модуля:
void SearchABibl(book* B, int N, char* A); //поиск книг по автору. Результат выводится на экран
void SearchHBibl(book* B, int N, char* H); //поиск книг по названию. Результат выводится на экран
void SearchAHBibl(book* B, int N, char* A, char* H) //поиск определённой книги.
Изначально создаётся общий заголовочный файл, включающий константы и типы данных. Общий заголовочный файл подключается в каждый модуль программы. В нашем случае он содержит описание структуры.
//const.h------------------------------------------------------------------------
#ifndef __CONST_H__
#define __CONST_H__
struct book
{
char header[50];
char author[50];
};
#endif
Заголовочные файлы для модулей:
//sort.h--------------------------------------------------------------------------
#include “const.h”
void SortABibl(book* B, int N);
void SortHBibl(book* B, int N);
//search.h------------------------------------------------------------------------
#include “const.h”
void SearchABibl(book* B, int N, char* A);
void SearchHBibl(book* B, int N, char* H);
void SearchAHBibl(book* B, int N, char* A, char* H);
Контрольные вопросы
Каковы этапы подготовки к выполнению программы на языке С++?
Как войти в ИСРП?
Каковы основные пункты меню и их назначение?
Какова структура программы на языке С++?
Для чего необходима директива препроцессору #include?
Какая функция используется для вывода информации на экран?
Какая функция используется для ввода данных с клавиатуры?
Что собой представляют спецификаторы преобразований?
Что называют идентификатором?
Как записывается оператор присваивания?
Требования к отчету
Отчет должен содержать:
конспект теоретической части;
лабораторное задание;
результаты выполнения программ.
Порядок выполнения лабораторной работы
Составить программу с использованием разбиения на отдельные файлы в соответствии со своим вариантом табл. 10.1.
Организовать на бригады по 3 человека.
Выполнить задание внутри бригады и собрать проект в единое целое в соответствии с вариантом табл. 10.2. Основной модуль должен содержать интерфейс пользователя и использовать все функции.
Варианты заданий
Таблица 10.1
Номер | Задание |
1,13 | Составить программу, описывающую домашнюю библиотеку из N книг (задаётся с клавиатуры) вида: автор, название, год издания, и выводящую на экран все книги нужного автора в библиографическом порядке(по году издания). |
2,14 | Составить программу, описывающую пасажирский самолёт из N мест (задаётся с клавиатуры) вида: имя пасажира, год рождения, пункт назначения, номер места, и выводящую на экран всех пасажиров в порядке следования номеров мест. |
3,15 | Составить программу, описывающую группу из N студентов (задаётся с клавиатуры) вида: имя студента, номер студенческого билета, средняя оценка, и выводящую на экран всех студентов в порядке успеваемости. |
4,16 | Составить программу, описывающую список покупок из N мест (задаётся с клавиатуры) вида: наименование, вес, цена, и выводящую на экран размер сумки и сумму, необходимую для похода в магазин. |
5,17 | Составить программу, описывающую набор комплек-тующих компьютера из N устройств (задаётся с кла-виатуры) вида: наименование, уровень шума (дБ), тепловыделение, и выводящую на экран все устройства в порядке уровня щума, общий уровень шума и общее тепловыделение. |
6,18 | Составить программу, описывающую покупки в кредит из N мест (задаётся с клавиатуры) вида: наименование, первоначальный взнос, срок, ежемесячный взнос (в %), и выводящую на экран все покупки в порядке истекания срока, а так же общую сумму первоначального взноса и ежемесячного за К-ый месяц. |
