Компьютерные науки - Учебники на русском языке - Скачать бесплатно
Итерация свойственна человеку,
рекурсия божественна.
- Л. Питер Дойч
Все нетривиальные программы собираются из нескольких
раздельно компилируемых единиц (их принято называть просто
файлами). В этой главе описано, как раздельно откомпилирован-
ные функции могут обращаться друг к другу, как такие функции
могут совместно пользоваться данными (разделять данные), и
как можно обеспечить согласованность типов, которые использу-
ются в разных файлах программы. Функции обсуждаются довольно
подробно. Сюда входят передача параметров, параметры по умол-
чанию, перегрузка имен функций, и, конечно же, описание и оп-
ределение функций. В конце описываются макросы.
4.1 Введение
Иметь всю программу в одном файле обычно невозможно,
поскольку коды стандартных библиотек и операционной системы
находятся где-то в другом месте. Кроме того, хранить весь
текст пользовательской программы в одном файле как правило
непрактично и неудобно. Способ организации программы в файлы
может помочь читающему охватить всю структуру программы, а
также может дать возможность компилятору реализовать эту
структуру. Поскольку единицей компиляции является файл, то во
всех случаях, когда в файл вносится изменение (сколь бы мало
оно ни было), весь файл нужно компилировать заново. Даже для
программы умеренных размеров время, затрачиваемое на переком-
пиляцию, можно значительно снизить с помощью разбиения прог-
раммы на файлы подходящих размеров.
Рассмотрим пример с калькулятором. Он был представлен в
виде одного исходного файла. Если вы его набили, то у вас на-
верняка были небольшие трудности с расположением описаний в
правильном порядке, и пришлось использовать по меньшей мере
одно "фальшивое" описание, чтобы компилятор смог обработать
взаимно рекурсивные функции expr(), term() и prim(). В тексте
уже отмечалось, что программа состоит из четырех частей (лек-
сического анализатора, программы синтаксического разбора,
таблицы имен и драйвера), но это никак не было отражено в
тексте самой программы. По сути дела, калькулятор был написан
по-другому. Так это не делается; даже если в этой программе
"на выброс" пренебречь всеми соображениями методологии прог-
раммирования, эксплуатации и эффективности компиляции, автор
все равно разобьет эту программу в 200 строк на несколько
файлов, чтобы программировать было приятнее.
Программа, состоящая из нескольких раздельно компилируе-
мых файлов, должна быть согласованной в смысле использования
имен и типов, точно так же, как и программа, состоящая из од-
ного исходного файла. В принципе, это может обеспечить и ком-
поновщик*. Компоновщик - это программа, стыкующая отдельно
скомпилированные части вместе. Компоновщик часто (путая) на-
зывают загрузчиком. В UNIX'е компоновщик называется ld. Одна-
ко компоновщики, имеющиеся в большинстве систем, обеспечивают
очень слабую поддержку проверки согласованности.
--------------------
* или линкер. (прим. перев.)
Программист может скомпенсировать недостаток поддержки
со стороны компоновщика, предоставив дополнительную информа-
цию о типах (описания). После этого согласованность программы
обеспечивается проверкой согласованности описаний, которые
- 90 -
находятся в отдельно компилируемых частях. Средства, которые
это обеспечивают, в вашей системе будут. С++ разработан так,
чтобы способствовать такой явной компоновке*.
--------------------
* C разработан так, чтобы в большинстве случаев позво-
лять осуществлять неявную компоновку. Применение C, однако,
возросло неимоверно, поэтому случаи, когда можно использовать
неявную линковку, сейчас составляют незначительное меньшинс-
тво. (прим. автора)
4.2 Компоновка
Если не указано иное, то имя, не являющееся локальным
для функции или класса, в каждой части программы, компилируе-
мой отдельно, должно относиться к одному и тому же типу, зна-
чению, функции или объекту. То есть, в программе может быть
только один нелокальный тип, значение, функция или объект с
этим именем. Рассмотрим, например, два файла:
// 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
программой является):
// file1.c:
int a;
int f() (* return a; *)
// file2.c:
int a;
int g() (* return f(); *)
Во-первых, file2.c не С++, потому что f() не была описа-
- 91 -
на, и поэтому компилятор будет недоволен. Во-вторых, (когда
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; *)
// file1.c:
const int a = 7;
inline int f() (* /* ... */ *)
struct s (* int a,b; *)
Раз правило "ровно одно определение" применяется к конс-
тантам, inline-функциям и определениям функций так же, как
оно применяется к функциям и переменным, то file1.c и file2.c
не могут быть частями одной С++ программы. Но если это так,
то как же два файла могут использовать одни и те же типы и
константы? Коротко, ответ таков: типы, константы и т.п. могут
определяться столько раз, сколько нужно, при условии, что они
определяются одинаково. Полный ответ несколько более сложен
(это объясняется в следующем разделе).
4.3 Заголовочные Файлы
Типы во всех описаниях одного и того же объекта должны
быть согласованными. Один из способов это достичь мог бы сос-
тоять в обеспечении средств проверки типов в компоновщике, но
большинство компоновщиков - образца 1950-х, и их нельзя изме-
нить по практическим соображениям*. Другой подход состоит в
обеспечении того, что исходный текст, как он передается на
рассмотрение компилятору, или согласован, или содержит инфор-
мацию, которая позволяет компилятору обнаружить несогласован-
ности. Один несовершенный, но простой способ достичь согласо-
ванности состоит во включении заголовочных файлов, содержащих
интерфейсную информацию, в исходные файлы, в которых содер-
жится исполняемый код и/или определения данных.
--------------------
* Легко изменить один компоновщик, но сделав это и напи-
сав программу, которая зависит от усовершенствований, как вы
будете переносить эту программу в другое место? (прим. авто-
ра)
- 92 -
Механизм включения с помощью #include - это чрезвычайно
простое средство обработки текста для сборки кусков исходной
программы в одну единицу (файл) для ее компиляции. Директива
#include "to_be_included"
замещает строку, в которой встретилось #include, содер-
жимым файла "to_be_included". Его содержимым должен быть ис-
ходный текст на С++, поскольку дальше его будет читать компи-
лятор. Часто включение обрабатывается отдельной программой,
называемой C препроцессором, которую команда CC вызывает для
преобразования исходного файла, который дал программист, в
файл без директив включения перед тем, как начать собственно
компиляцию. В другом варианте эти директивы обрабатывает ин-
терфейсная система компилятора по мере того, как они встреча-
ются в исходном тексте. Если программист хочет посмотреть на
результат директив включения, можно воспользоваться командой
CC -E file.c
для препроцессирования файла file.c точно также, как это
сделала бы CC перед запуском собственно компилятора. Для
включения файлов из стандартной директории включения вместо
кавычек используются угловые скобки < и >. Например:
#include //из стандартной директории включения
#define "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;
- 93 -
Определения сложных константных объектов const tbl[]=(*/*
... */ *)
В системе UNIX принято, что заголовочные файлы имеют
суффикс (расширение) .h. Файлы, содержащие определение данных
или функций, должны иметь суффикс .c. Такие файлы часто назы-
вают, соответственно, ".h файлы" и ".c файлы". В #4.7 описы-
ваются макросы. Следует заметить, что в С++ макросы гораздо
менее полезны, чем в C, поскольку С++ имеет такие языковые
конструкции, как const для определения констант и inline для
исключения расходов на вызов функции.
Причина того, почему в заголовочных файлах допускается
определение простых констант, но не допускается определение
сложных константных объектов, прагматическая. В принципе,
сложность тут только в том, чтобы сделать допустимым дублиро-
вание определений переменных (даже определения функций можно
было бы дублировать). Однако для компоновщиков старого образ-
ца слишком трудно проверять тождественность нетривиальных
констант и убирать ненужные повторы. Кроме того, простые слу-
чаи гораздо более обиходны и потому более важны для генерации
хорошего кода.
4.3.1 Один Заголовочный Файл
Проще всего решить проблему разбиения программы на нес-
колько файлов поместив функции и определения данных в подхо-
дящее число исходных файлов и описав типы, необходимые для их
взаимодействия, в одном заголовочном файле, который включает-
ся во все остальные файлы. Для программы калькулятора можно
использовать четыре .c файла: lex.c, syn.c, table.c и main.c,
и заголовочный файл dc.h, содержащий описания всех имен, ко-
торые используются более чем в одном .c файле:
// dc.h: общие описания для калькулятора
enum token_value (*
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*', DIV='/',
PRINT=';', ASSIGN='=', LP='(', RP=')'
*);
extern int no_of_errors;
extern double error(char* s);
extern token_value get_token();
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern double expr();
extern double term();
extern double prim();
struct name (*
char* string;
name* next;
double value;
*);
extern name* look(char* p, int ins = 0);
inline name* insert(char* s) (* return look(s,1); *)
Если опустить фактический код, то lex.c будет выглядеть
примерно так:
// lex.c: ввод и лексический анализ
#include "dc.h"
- 94 -
#include
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() (* /* ... */ *)
Заметьте, что такое использование заголовочных файлов
гарантирует, что каждое описание в заголовочном файле объек-
та, определенного пользователем, будет в какой-то момент
включено в файл, где он определяется. Например, при компиля-
ции lex.c компилятору будет передано:
extern token_value get_token();
// ...
token_value get_token() (* /* ... */ *)
Это обеспечивает то, что компилятор обнаружит любую не-
согласованность в типах, указанных для имени. Например, если
бы get_token() была описана как возвращающая token_value, но
при этом определена как возвращающая int, компиляция lex.c не
прошла бы изза ошибки несоответствия типов.
Файл syn.c будет выглядеть примерно так:
// syn.c: синтаксический анализ и вычисление
#include "dc.h"
double prim() (* /* ... */ *)
double term() (* /* ... */ *)
double expr() (* /* ... */ *)
Файл table.c будет выглядеть примерно так:
// table.c: таблица имен и просмотр
#include "dc.h"
extern char* strcmp(const char*, const char*);
extern char* strcpy(char*, const char*);
extern int strlen(const char*);
const TBLSZ = 23;
name* table[TBLSZ];
name* look(char* p; int ins) (* /* ... */ *)
Заметьте, что table.c сам описывает стандартные функции
для работы со строками, поэтому никакой проверки согласован-
ности этих описаний нет. Почти всегда лучше включать заголо-
вочный файл, чем описывать имя в .c файле как extern. При
этом может включаться "слишком много", но это обычно не ока-
зывает серьезного влияния на время, необходимое для компиля-
ции, и как правило экономит время программиста. В качестве
примера этого, обратите внимание на то, как strlen() заново
описывается в main() (ниже). Это лишние нажатия клавиш и воз-
можный источник неприятностей, поскольку компилятор не может
проверить согласованность этих двух определений. На самом де-
ле, этой сложности можно было бы избежать, будь все описания
extern помещены в dc.h, как и предлагалось сделать. Эта "неб-
режность" сохранена в программе, поскольку это очень типично
для C программ, очень соблазнительно для программиста, и чаще
приводит, чем не приводит, к ошибкам, которые трудно обнару-
жить, и к программам, с которыми тяжело работать. Вас предуп-
редили!
- 95 -
И main.c, наконец, выглядит так:
// main.c: инициализация, главный цикл и обработка ошибок
#include "dc.h"
int no_of_errors;
double error(char* s) (* /* ... */ *)
extern int strlen(const char*);
main(int argc, char* argv[]) (* /* ... */ *)
Важный случай, когда размер заголовочных файлов стано-
вится серьезной помехой. Набор заголовочных файлов и
библиотеку можно использовать для расширения языка множеством
обще- и специальноприкладных типов (см. Главы 5-8). В таких
случаях не принято осуществлять чтение тысяч строк заголовоч-
ных файлов в начале каждой компиляции. Содержание этих файлов
обычно "заморожено" и изменяется очень нечасто. Наиболее по-
лезным может оказаться метод затравки компилятора содержанием
этих заголовочных фалов. По сути, создается язык специального
назначения со своим собственным компилятором. Никакого стан-
дартного метода создания такого компилятора с затравкой не
принято.
4.3.2 Множественные Заголовочные Файлы
Стиль разбиения программы с одним заголовочным файлом
наиболее пригоден в тех случаях, когда программа невелика и
ее части не предполагается использовать отдельно. Поэтому то,
что невозможно установить, какие описания зачем помещены в
заголовочный файл, несущественно. Помочь могут комментарии.
Другой способ - сделать так, чтобы каждая часть программы
имела свой заголовочный файл, в котором определяются предос-
тавляемые этой частью средства. Тогда каждый .c файл имеет
соответствующий .h файл, и каждый .c файл включает свой собс-
твенный (специфицирующий то, что в нем задается) .h файл и,
возможно, некоторые другие .h файлы (специфицирующие то, что
ему нужно).
Рассматривая организацию калькулятора, мы замечаем, что
error() используется почти каждой функцией программы, а сама
использует только . Это обычная для функции ошибок
ситуация, поэтому error() следует отделить от main():
// error.h: обработка ошибок
extern int no_errors;
extern double error(char* s);
// error.c
#include
#include "error.h"
int no_of_errors;
double error(char* s) (* /* ... */ *)
При таком стиле использования заголовочных файлов .h
файл и связанный с ним .c файл можно рассматривать как мо-
дуль, в котором .h файл задает интерфейс, а .c файл задает
реализацию.
Таблица символов не зависит от остальной части калькуля-
- 96 -
тора за исключением использования функции ошибок. Это можно
сделать явным:
// table.h: описания таблицы имен
struct name (*
char* string;
name* next;
double value;
*);
extern name* look(char* p, int ins = 0);
inline name* insert(char* s) (* return look(s,1); *)
// table.c: определения таблицы имен
#include "error.h"
#include
#include "table.h"
const TBLSZ = 23;
name* table[TBLSZ];
name* look(char* p; int ins) (* /* ... */ *)
Заметьте, что описания функций работы со строками теперь
включаются из . Это исключает еще один возможный
источник ошибок.
// lex.h: описания для ввода и лексического анализа
enum token_value (*
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*', DIV='/',
PRINT=';', ASSIGN='=', LP='(', RP=')'
*);
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern token_value get_token();
Этот интерфейс лексического анализатора достаточно бес-
порядочен. Недостаток в надлежащем типе лексемы обнаруживает
себя в необходимости давать пользователю get_token() факти-
ческие лексические буферы number_value и name_string.
// lex.c: определения для ввода и лексического анализа
#include
#include
#include "error.h"
#include "lex.h"
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() (* /* ... */ *)
Интерфейс синтаксического анализатора совершенно прозра-
чен:
// syn.c: описания для синтаксического анализа и вычисления
extern double expr();
extern double term();
- 97 -
extern double prim();
// syn.c: определения для синтаксического анализа и
// вычисления
#include "error.h"
#include "lex.h"
#include "syn.h"
double prim() (* /* ... */ *)
double term() (* /* ... */ *)
double expr() (* /* ... */ *)
Главная программа, как всегда, тривиальна:
// main.c: главная программа
#include
#include "error.h"
#include "lex.h"
#include "syn.h"
#include "table.h"
#include
main(int argc, char* argv[]) (* /* ... */ *)
Сколько заголовочных файлов использовать в программе,
зависит от многих факторов. Многие из этих факторов сильнее
связаны с тем, как ваша система работает с заголовочными фай-
лами, нежели с С++. Например, если в вашем редакторе нет
средств, позволяющих одновременно видеть несколько файлов,
использование большого числа файлов становится менее привле-
кательным. Аналогично, если открывание и чтение 10 файлов по
50 строк в каждом требует заметно больше времени, чем чтение
одного файла в 500 строк, вы можете дважды подумать, прежде
чем использовать в небольшом проекте стиль множественных за-
головочных файлов. Слово предостережения: набор из десяти за-
головочных файлов плюс стандартные заголовочные файлы обычно
легче поддаются управлению. С другой стороны, если вы разбили
описания в большой программе на логически минимальные по раз-
меру заголовочные файлы (помещая каждое описание структуры в
свой отдельный файл и т.д.), у вас легко может получиться не-
разбериха из сотен файлов.
4.3.3 Сокрытие Данных
Используя заголовочные файлы пользователь может опреде-
лять явный интерфейс, чтобы обеспечить согласованное исполь-
зование типов в программе. С другой стороны, пользователь
может обойти интерфейс, задаваемый заголовочным файлом, вводя
в .c файлы описания extern.
Заметьте, что такой стиль компоновки не рекомендуется:
// file1.c: // "extern" не используется
int a = 7;
const c = 8;
void f(long) (* /* ... */ *)
// file2.c: // "extern" в .c файле
extern int a;
extern const c;
extern f(int);
int g() (* return f(a+c); *)
Поскольку описания extern в file2.c не включаются вместе
с определениями в файле file1.c, компилятор не может прове-
- 98 -
рить согласованность этой программы. Следовательно, если
только загрузчик не окажется гораздо сообразительнее средне-
го, две ошибки в этой программе останутся, и их придется ис-
кать программисту.
Пользователь может защитить файл от такой недисциплини-
рованной компоновки, описав имена, которые не предназначены
для общего пользования, как static, чтобы их областью види-
мости был файл, и они были скрыты от остальных частей прог-
раммы. Например:
// table.c: определения таблицы имен
#include "error.h"
#include
#include "table.h"
const TBLSZ = 23;
static name* table[TBLSZ];
name* look(char* p; int ins) (* /* ... */ *)
Это гарантирует, что любой доступ к table действительно
будет осуществляться именно через look(). "Прятать" константу
TBLSZ не обязательно.
4.4 Файлы как Модули
В предыдущем разделе .c и .h файлы вместе определяли
часть программы. Файл .h является интерфейсом, который ис-
пользуют другие части программы, .c файл задает реализацию.
Такой объект часто называют модулем. Доступными делаются
только те имена, которые необходимо знать пользователю, ос-
тальные скрыты. Это качество часто называют сокрытием данных,
хотя данные - лишь часть того, что может быть скрыто. Модули
такого вида обеспечивают большую гибкость. Например, реализа-
ция может состоять из одного или более .c файлов, и в виде .h
файлов может быть предоставлено несколько интерфейсов. Инфор-
мация, которую пользователю знать не обязательно, искусно
скрыта в .c файлах. Если важно, что пользователь не должен
точно знать, что содержится в .c файлах, не надо делать их
доступными в исходом виде. Достаточно эквивалентных им выход-
ных файлов компилятора (.o файлов).
Иногда возникает сложность, состоящая в том, что подоб-
ная гибкость достигается без формальной структуры. Сам язык
не распознает такой модуль как объект, и у компилятора нет
возможности отличить .h файлы, определяющие имена, которые
должны использовать другие модули (экспортируемые), от .h
файлов, которые описывают имена из других модулей (импортиру-
емые).
В других случаях может возникнуть та проблема, что мо-
дуль определяет множество объектов, а не новый тип. Например,
модуль table определяет одну таблицу, и если вам нужно две
таблицы, то нет простого способа задать вторую таблицу с по-
мощью понятия модуля. Решение этой проблемы приводится в Гла-
ве 5.
Каждый статически размещенный объект по умолчанию иници-
ализируется нулем, программист может задать другие
(константные) значения. Это только самый примитивный вид ини-
циализации. К счастью, с помощью классов можно задать код,
который выполняется для инициализации перед тем, как модуль
какимлибо образом используется, и/или код, который запускает-
ся для очистки после последнего использования модуля, см.
#5.5.2.
- 99 -
4.5 Как Создать Библиотеку
Фразы типа "помещен в библиотеку" и "ищется в какой-то
библиотеке" используются часто (и в этой книге, и в других),
но что это означает для С++ программы? К сожалению, ответ за-
висит от того, какая операционная система используется; в
этом разделе объясняется, как создать библиотеку в 8-ой вер-
сии системы UNIX. Другие системы предоставляют аналогичные
возможности.
Библиотека в своей основе является множеством .o файлов,
полученных в результате компиляции соответствующего множества
.c файлов. Обычно имеется один или более .h файлов, в которых
содержатся описания для использования этих .o файлов. В ка-
честве примера рассмотрим случай, когда нам надо задать
(обычным способом) набор математических функций для некоторо-
го неопределенного множества пользователей. Заголовочный файл
мог бы выглядеть примерно так:
extern double sqrt(double); // подмножество
extern double sin(double);
extern double cos(double);
extern double exp(double);
extern double log(double);
а определения этих функций хранились бы, соответственно,
в файлах sqrt.c, sin.c, cos.c, exp.c и log.c.
Библиотеку с именем math.h можно создать, например, так:
$ CC -c sqrt.c sin.c cos.c exp.c log.c
$ ar cr math.a sqrt.o sin.o cos.o exp.o log.o
$ ranlib math.a
Вначале исходные файлы компилируются в эквивалентные им
объектные файлы. Затем используется команда ar, чтобы создать
архив с именем math.a. И, наконец, этот архив индексируется
для ускорения доступа. Если в вашей системе нет ranlib коман-
ды, значит она вам, вероятно, не понадобится. Подробности
посмотрите, пожалуйста, в вашем руководстве в разделе под за-
головком ar. Использовать библиотеку можно, например, так:
$ CC myprog.c math.a
Теперь разберемся, в чем же преимущества использования
math.a перед просто непосредственным использованием .o фай-
лов? Например:
$ CC myprog.c sqrt.o sin.o cos.o exp.o log.o
Для большинства программ определить правильный набор .o
файлов, несомненно, непросто. В приведенном выше примере они
включались все, но если функции в myprog.c вызывают только
функции sqrt() и cos(), то кажется, что будет достаточно
$ CC myprog.c sqrt.o cos.o
Но это не так, поскольку cos.c использует sin.c.
Компоновщик, вызываемый командой CC для обработки .a
файла (в данном случае, файла math.a) знает, как из того мно-
жества, которое использовалось для создания .a файла, извлечь
только необходимые .o файлы.
Другими словами, используя библиотеку можно включать
много определений с помощью одного имени (включения определе-
ний функций и переменных, используемых внутренними функциями,
- 100 -
никогда не видны пользователю), и, кроме того, обеспечить,
что в результате в программу будет включено минимальное коли-
чество определений.
4.6 Функции
Обычный способ сделать что-либо в С++ программе - это
вызвать функцию, которая это делает. Определение функции яв-
ляется способом задать то, как должно делаться некоторое
действие. Функция не может быть вызвана, пока она не описана.
4.6.1 Описания Функций
Описание функции задает имя функции, тип возвращаемого
функцией значения (если таковое есть) и число и типы парамет-
ров, которые должны быть в вызове функции. Например:
extern double sqrt(double);
extern elem* next_elem();
extern char* strcpy(char* to, const char* from);
extern void exit(int);
Семантика передачи параметров идентична семантике иници-
ализации. Проверяются типы параметров, и когда нужно произво-
дится неявное преобразование типа. Например, если были заданы
предыдущие определения, то
double sr2 = sqrt(2);
будет правильно обращаться к функции sqrt() со значением
с плавающей точкой 2.0. Значение такой проверки типа и преоб-
разования типа огромно.
Описание функции может содержать имена параметров. Это
может помочь читателю, но компилятор эти имена просто игнори-
рует.
4.6.2 Определения Функций
Каждая функция, вызываемая в программе, должна быть где-
то определена (только один раз). Определение функции - это
описание функции, в котором приводится тело функции. Напри-
мер:
extern void swap(int*, int*); // описание
void swap(int*, int*) // определение
(*
int t = *p;
*p =*q;
*q = t;
*)
Чтобы избежать расходов на вызов функции, функцию можно
описать как inline (#1.12), а чтобы обеспечить более быстрый
доступ к параметрам, их можно описать как register (#2.3.11).
Оба средства могут использоваться неправильно, и их следует
избегать везде где есть какие-либо сомнения в их полезности.
4.6.3 Передача Параметров
Когда вызывается функция, дополнительно выделяется па-
мять под ее формальные параметры, и каждый формальный пара-
метр инициализируется соответствующим ему фактическим пара-
метром. Семантика передачи параметров идентична семантике
инициализации. В частности, тип фактического параметра сопос-
тавляется с типом формального параметра, и выполняются все
- 101 -
стандартные и определенные пользователем преобразования ти-
пов. Есть особые правила для передачи векторов (#4.6.5),
средство передавать параметр без проверки типа параметра
(#4.6.8) и средство для задания параметров по умолчанию
(#4.6.6). Рассмотрим
void f(int val, int& ref)
(*
val++;
ref++;
*)
Когда вызывается f(), val++ увеличивает локальную копию
первого фактического параметра, тогда как ref++ увеличивает
второй фактический параметр. Например:
int i = 1;
int j = 1;
f(i,j);
увеличивает j, но не i. Первый параметр - i, передается
по значению, второй параметр - j, передается по ссылке. Как
уже отмечалось в #2.3.10, использование функций, которые из-
меняют переданные по ссылке параметры, могут сделать програм-
му трудно читаемой, и их следует избегать (но см. #6.5 и
#8.4). Однако передача большого объекта по ссылке может быть
гораздо эффективнее, чем передача его по значению. В этом
случае параметр можно описать как const, чтобы указать, что
ссылка применяется по соображениям эффективности, а также
чтобы не позволить вызываемой функции изменять значение объ-
екта:
void f(const large& arg)
(*
// значение "arg" не может быть изменено
*)
Аналогично, описание параметра указателя как const сооб-
щает читателю, что значение объекта, указываемого указателем,
функцией не изменяется. Например:
extern int strlen(const char*); // из
extern char* strcpy(char* to, const char* from);
extern int strcmp(const char*, const char*);
Важность такой практики возрастает с размером программы.
Заметьте, что семантика передачи параметров отлична от
семантики присваивания. Это важно для const параметров, ссы-
лочных параметров и параметров некоторых типов, определяемых
пользователем (#6.6).
4.6.4 Возврат Значения
Из функции, которая не описана как void, можно (и долж-
но) возвращать значение. Возвращаемое значение задается опе-
ратором return. Например:
int fac(int n) (*return (n>1) ? n*fac(n-1) : 1; *)
В функции может быть больше одного оператора return:
int fac(int n)
(*
if (n > 1)
return n*fac(n-1);
else
return 1;
- 102 -
*)
Как и семантика передачи параметров, семантика возврата
функцией значения идентична семантике инициализации. Возвра-
щаемое значение рассматривается как инициализатор переменной
возвращаемого типа. Тип возвращаемого выражения проверяется
на согласованность с возвращаемым типом и выполняются все
стандартные и определенные пользователем преобразования ти-
пов. Например:
double f()
(*
// ...
return 1; // неявно преобразуется к double(1)
*)
Каждый раз, когда вызывается функция, создается новая
копия ее параметров и автоматических переменных. После возв-
рата из функции память используется заново, поэтому возвра-
щать указатель на локальную переменную неразумно. Содержание
указываемого места изменится непредсказуемо:
int* f() (*
int local = 1;
// ...
return &local; // так не делайте
*)
Эта ошибка менее обычна, чем эквивалентная ошибка при
использовании ссылок:
int& f() (*
int local = 1;
// ...
return local; // так не делайте
*)
К счастью, о таких возвращаемых значениях предупреждает
компилятор. Вот другой пример:
int& f() (* return 1;*) // так не делайте
4.6.5 Векторные Параметры
Если в качестве параметра функции используется вектор,
то передается указатель на его первый элемент. Например:
int strlen(const char*);
void f()
(*
char v[] = "a vector"
strlen(v);
strlen("Nicholas");
*);
Иначе говоря, при передаче как параметр параметр типа
T[] преобразуется к T*. Следовательно, присваивание элементу
векторного параметра изменяет значение элемента вектора, ко-
торый является параметром. Другими словами, вектор отличается
от всех остальных типов тем, что вектор не передается (и не
может передаваться) по значению.
Размер вектора недоступен вызываемой функции. Это может
быть неудобно, но эту сложность можно обойти несколькими спо-
собами. Строки оканчиваются нулем, поэтому их размер можно
легко вычислить. Для других векторов можно передавать второй
- 103 -
параметр, который задает размер, или определить тип, содержа-
щий указатель и индикатор длины, и передавать его вместо
просто вектора (см. также #1.11). Например:
void compute1(int* vec_ptr, int vec_size); // один способ
struct vec (* // другой способ
int* ptr;
int size;
*);
void compute2(vec v);
С многомерными массивами все хитрее, но часто можно
вместо них использовать векторы указателей, которые не требу-
ют специального рассмотрения. Например:
char* day[] = (*
"mon", "tue", "wed", "thu", "fri", "sat", "sun"
*);
С другой стороны, рассмотрим определение функции, кото-
рая работает с двумерными матрицами. Если размерность извест-
на на стадии компиляции, то никаких проблем нет:
void print_m34(int m[3][4])
(*
for (int i = 0; i<3; i++) (*
for (int j = 0; j<4; j++)
cout << " " << m[i][j];
cout << "\n";
*)
*)
Матрица, конечно, все равно передается как указатель, а
размерности используются просто для удобства записи.
|