Все нетривиальные программы собираются из нескольких раздельно
компилируемых единиц (их принято называть просто файлами). В этой
главе описано, как раздельно откомпилированные функции могут
обращаться друг к другу, как такие функции могут совместно
пользоваться данными (разделять данные), и как можно обеспечить
согласованность типов, которые используются в разных файлах
программы. Функции обсуждаются довольно подробно. Сюда входят
передача параметров, параметры по умолчанию, перегрузка имен
функций, и, конечно же, описание и определение функций. В конце
описываются макросы.
Иметь всю программу в одном файле обычно невозможно, поскольку
коды стандартных библиотек и операционной системы находятся где-то
в другом месте. Кроме того, хранить весь текст пользовательской
программы в одном файле как правило непрактично и неудобно. Способ
организации программы в файлы может помочь читающему охватить всю
структуру программы, а также может дать возможность компилятору
реализовать эту структуру. Поскольку единицей компиляции является
файл, то во всех случаях, когда в файл вносится изменение (сколь бы
мало оно ни было), весь файл нужно компилировать заново. Даже для
программы умеренных размеров время, затрачиваемое на
перекомпиляцию, можно значительно снизить с помощью разбиения
программы на файлы подходящих размеров.
Если не указано иное, то имя, не являющееся локальным для функции
или класса, в каждой части программы, компилируемой отдельно,
должно относиться к одному и тому же типу, значению, функции или
объекту. То есть, в программе может быть только один нелокальный
тип, значение, функция или объект с этим именем. Рассмотрим,
например, два файла:
4.1 Введение
Рассмотрим пример с калькулятором. Он был представлен в виде
одного исходного файла. Если вы его набили, то у вас наверняка были
небольшие трудности с расположением описаний в правильном порядке,
и пришлось использовать по меньшей мере одно "фальшивое" описание,
чтобы компилятор смог обработать взаимно рекурсивные функции
expr(), term() и prim(). В тексте уже отмечалось, что программа
состоит из четырех частей (лексического анализатора, программы
синтаксического разбора, таблицы имен и драйвера), но это никак не
было отражено в тексте самой программы. По сути дела, калькулятор
был написан по-другому. Так это не делается; даже если в этой
программе "на выброс" пренебречь всеми соображениями методологии
программирования, эксплуатации и эффективности компиляции, автор
все равно разобьет эту программу в 200 строк на несколько файлов,
чтобы программировать было приятнее.
Программа, состоящая из нескольких раздельно компилируемых
файлов, должна быть согласованной в смысле использования имен и
типов, точно так же, как и программа, состоящая из одного исходного
файла. В принципе, это может обеспечить и компоновщик*1. Компоновщик
- это программа, стыкующая отдельно скомпилированные части вместе.
Компоновщик часто (путая) называют загрузчиком. В UNIX'е
компоновщик называется ld. Однако компоновщики, имеющиеся в
большинстве систем, обеспечивают очень слабую поддержку проверки
согласованности.
Программист может скомпенсировать недостаток поддержки со стороны
компоновщика, предоставив дополнительную информацию о типах
(описания). После этого согласованность программы обеспечивается
проверкой согласованности описаний, которые находятся в отдельно
компилируемых частях. Средства, которые это обеспечивают, в вашей
системе будут. C++ разработан так, чтобы способствовать такой явной
компоновке*2.
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++ программой (хотя 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; }
// file1.c:
const int a = 7;
inline int f() { /* ... */ }
struct s { int a,b; }
Раз правило "ровно одно определение" применяется к константам,
inline-функциям и определениям функций так же, как оно применяется
к функциям и переменным, то file1.c и file2.c не могут быть частями
одной C++ программы. Но если это так, то как же два файла могут
использовать одни и те же типы и константы? Коротко, ответ таков:
типы, константы и т.п. могут определяться столько раз, сколько
нужно, при условии, что они определяются одинаково. Полный ответ
несколько более сложен (это объясняется в следующем разделе).
4.3 Заголовочные Файлы
4.3.1 Один Заголовочный Файл | |
4.3.2 Множественные Заголовочные Файлы | |
4.3.3 Скрытие Данных |
Типы во всех описаниях одного и того же объекта должны быть
согласованными. Один из способов это достичь мог бы состоять в
обеспечении средств проверки типов в компоновщике, но большинство
компоновщиков - образца 1950-х, и их нельзя изменить по
практическим соображениям *3. Другой подход состоит в обеспечении того, что исходный текст, как он передается на рассмотрение
компилятору, или согласован, или содержит информацию, которая
позволяет компилятору обнаружить несогласованности. Один
несовершенный, но простой способ достичь согласованности состоит во
включении заголовочных файлов, содержащих интерфейсную информацию,
в исходные файлы, в которых содержится исполняемый код и/или
определения данных.
Механизм включения с помощью #include - это чрезвычайно простое
средство обработки текста для сборки кусков исходной программы в
одну единицу (файл) для ее компиляции. Директива
#include "to_be_included"
CC -E file.c
#include // из стандартной директории включения #define "myheader.h" // из текущей директории
#include < stream.h > // не найдет
Определения типов | 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[] = { /* ... */ } |
Проще всего решить проблему разбиения программы на несколько файлов поместив функции и определения данных в подходящее число исходных файлов и описав типы, необходимые для их взаимодействия, в одном заголовочном файле, который включается во все остальные файлы. Для программы калькулятора можно использовать четыре .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: ввод и лексический анализ #include "dc.h" #include token_value curr_tok; double number_value; char name_string[256]; token_value get_token() { /* ... */ }
extern token_value get_token(); // ... token_value get_token() { /* ... */ }
// syn.c: синтаксический анализ и вычисление #include "dc.h" double prim() { /* ... */ } double term() { /* ... */ } double expr() { /* ... */ }
// 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) { /* ... */ }
// main.c: инициализация, главный цикл и обработка ошибок #include "dc.h" int no_of_errors; double error(char* s) { /* ... */ } extern int strlen(const char*); main(int argc, char* argv[]) { /* ... */ }
Стиль разбиения программы с одним заголовочным файлом наиболее
пригоден в тех случаях, когда программа невелика и ее части не
предполагается использовать отдельно. Поэтому то, что невозможно
установить, какие описания зачем помещены в заголовочный файл,
несущественно. Помочь могут комментарии. Другой способ - сделать
так, чтобы каждая часть программы имела свой заголовочный файл, в
котором определяются предоставляемые этой частью средства. Тогда
каждый .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) { /* ... */ }
// 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();
// 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(); 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[]) { /* ... */ }
Используя заголовочные файлы пользователь может определять явный
интерфейс, чтобы обеспечить согласованное использование типов в
программе. С другой стороны, пользователь может обойти интерфейс,
задаваемый заголовочным файлом, вводя в .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); }
// table.c: определения таблицы имен #include "error.h" #include #include "table.h" const TBLSZ = 23; static name* table[TBLSZ]; name* look(char* p; int ins) { /* ... */ }
В предыдущем разделе .c и .h файлы вместе определяли часть
программы. Файл .h является интерфейсом, который используют другие
части программы; .c файл задает реализацию. Такой объект часто
называют модулем. Доступными делаются только те имена, которые
необходимо знать пользователю, остальные скрыты. Это качество часто
называют скрытием данных, хотя данные - лишь часть того, что может
быть скрыто. Модули такого вида обеспечивают большую гибкость.
Например, реализация может состоять из одного или более .c файлов,
и в виде .h файлов может быть предоставлено несколько интерфейсов.
Информация, которую пользователю знать не обязательно, искусно
скрыта в .c файлах. Если важно, что пользователь не должен точно
знать, что содержится в .c файлах, не надо делать их доступными в
исходом виде. Достаточно эквивалентных им выходных файлов
компилятора (.o файлов).
Фразы типа "помещен в библиотеку" и "ищется в какой-то
библиотеке" используются часто (и в этой книге, и в других), но что
это означает для C++ программы? К сожалению, ответ зависит от того,
какая операционная система используется; в этом разделе
объясняется, как создать библиотеку в 8-ой версии системы UNIX.
Другие системы предоставляют аналогичные возможности.
Иногда возникает сложность, состоящая в том, что подобная
гибкость достигается без формальной структуры. Сам язык не
распознает такой модуль как объект, и у компилятора нет возможности
отличить .h файлы, определяющие имена, которые должны использовать
другие модули (экспортируемые), от .h файлов, которые описывают
имена из других модулей (импортируемые).
В других случаях может возникнуть та проблема, что модуль
определяет множество объектов, а не новый тип. Например, модуль
table определяет одну таблицу, и если вам нужно две таблицы, то нет
простого способа задать вторую таблицу с помощью понятия модуля.
Решение этой проблемы приводится в Главе 5.
Каждый статически размещенный объект по умолчанию
инициализируется нулем, программист может задать другие
(константные) значения. Это только самый примитивный вид
инициализации. К счастью, с помощью классов можно задать код,
который выполняется для инициализации перед тем, как модуль каким-
либо образом используется, и/или код, который запускается для
очистки после последнего использования модуля; см. #5.5.2.
4.5 Как Создать Библиотеку
Библиотека в своей основе является множеством .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. И, наконец, этот архив индексируется для ускорения доступа.
Если в вашей системе нет команды runlib, значит она вам, вероятно,
не понадобится. Подробности посмотрите, пожалуйста, в вашем
руководстве в разделе под заголовком 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
файлы.
Другими словами, используя библиотеку можно включать много
определений с помощью одного имени (включения определений функций и
переменных, используемых внутренними функциями, никогда не видны
пользователю), и, кроме того, обеспечить, что в результате в
программу будет включено минимальное количество определений.
4.6 Функции
Обычный способ сделать что-либо в C++ программе - это вызвать
функцию, которая это делает. Определение функции является способом
задать то, как должно делаться некоторое действие. Функция не может
быть вызвана, пока она не описана.
Описание функции задает имя функции, тип возвращаемого функцией
значения (если таковое есть) и число и типы параметров, которые
должны быть в вызове функции. Например:
Каждая функция, вызываемая в программе, должна быть где-то
определена (только один раз). Определение функции - это описание
функции, в котором приводится тело функции. Например:
Когда вызывается функция, дополнительно выделяется память под ее
формальные параметры, и каждый формальный параметр инициализируется
соответствующим ему фактическим параметром. Семантика передачи
параметров идентична семантике инициализации. В частности, тип
фактического параметра сопоставляется с типом формального
параметра, и выполняются все стандартные и определенные
пользователем преобразования типов. Есть особые правила для
передачи векторов (#4.6.5), средство передавать параметр без проверки (#4.6.8) и средство для задания параметров по умолчанию (#4.6.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 Передача Параметров
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" не может быть изменено }
extern int strlen(const char*); // из extern char* strcpy(char* to, const char* from); extern int strcmp(const char*, const char*);
Из функции, которая не описана как void, можно (и должно) возвращать значение. Возвращаемое значение задается оператором return. Например:
int fac(int n) {return (n>1) ? n*fac(n-1) : 1; }
int fac(int n) { if (n > 1) return n*fac(n-1); else return 1; }
double f() { // ... return 1; // неявно преобразуется к double(1) }
int* f() { int local = 1; // ... return &local; // так не делайте }
int& f() { int local = 1; // ... return local; // так не делайте }
int& f() { return 1;} // так не делайте
Если в качестве параметра функции используется вектор, то передается указатель на его первый элемент. Например:
int strlen(const char*); void f() { char v[] = "a vector" strlen(v); strlen("nicholas"); };
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"; } }
void print_mi4(int m[][4], int dim1) { for (int i = 0; i
Часто в самом общем случае функции требуется больше параметров, чем в самом простом и более употребительном случае. Например, в библиотеке потоков есть функция hex(), порождающая строку с шестнадцатиричным представлением целого. Второй параметр используется для задания числа символов для представления первого параметра. Если число символов слишком мало для представления целого, происходит усечение, если оно слишком велико, то строка дополняется пробелами. Часто программист не заботится о числе символов, необходимых для представления целого, поскольку символов достаточно. Поэтому для нуля в качестве второго параметра определено значение "использовать столько символов, сколько нужно". Чтобы избежать засорения программы вызовами вроде hex(i,0), функция описывается так:
extern char* hex(long, int =0);
cout << "**" << hex(31) << hex(32,3) << "**";
cout << "**" << hex(31,0) << hex(32,3) << "**";
** 1f 20**
int f(int, int =0, char* =0); // ok int g(int =0, int =0, char*); // ошибка int f(int =0, int, char* =0); // ошибка
int nasty(char*=0); // синтаксическая ошибка
Как правило, давать разным функциям разные имена - мысль хорошая, но когда некоторые функции выполняют одинаковую работу над объектами разных типов, может быть более удобно дать им одно и то же имя. Использование одного имени для различных действий над различными типами называется перегрузкой (overloading). Метод уже используется для основных операций C++: у сложения существует только одно имя, +, но его можно применять для сложения значений целых, плавающих и указательных типов. Эта идея легко расширяется на обработку операций, определенных пользователем, то есть, функций. Чтобы уберечь программиста от случайного повторного использования имени, имя может использоваться более чем для одной функции только если оно сперва описано как перегруженное. Например:
overload print; void print(int); void print(char*);
overload print(double), print(int); void f(); { print(1); print(1.0); }
overload print(double), print(long); void f(int a); { print(a); }
overload pow; int pow(int, int); double pow(double, double); // из complex pow(double, complex); // из complex pow(complex, int); complex pow(complex, double); complex pow(complex, complex);
Для некоторых функций невозможно задать число и тип всех параметров, которые можно ожидать в вызове. Такую функцию описывают завершая список описаний параметров многоточием (...), что означает "и может быть, еще какие-то параметры". Например:
int printf(char* ...);
printf("Hello, world\n"); printf("Мое имя %s %s\n", first_name, second_name); printf("%d + %d = %d\n",2,3,5);
printf("Мое имя %s %s\n",2);
extern int fprintf(FILE*, char* ...); // из extern int execl(char* ...); // из extern int abort(...); // из
void error(int ...); main(int argc, char* argv[]) { switch(argc) { case 1: error(0,argv[0],0); break; case 2: error(0,argv[0],argv[1],0); default: error(1,argv[0],"с",dec(argc-1),"параметрами",0); } }
#include void error(int n ...) /* "n" с последующим списком char*, оканчивающихся нулем */ { va_list ap; va_start(ap,n); // раскрутка arg for (;;) { char* p = va_arg(ap,char*); if(p == 0) break; cerr << p << " "; } va_end(ap); // очистка arg cerr << "\n"; if (n) exit(n); }
С функцией можно делать только две вещи: вызывать ее и брать ее адрес. Указатель, полученный взятием адреса функции, можно затем использовать для вызова этой функции. Например:
void error(char* p) { /* ... */ } void (*efct)(char*); // указатель на функцию void f() { efct = &error; // efct указывает на error (*efct)("error"); // вызов error через efct }
void (*pf)(char*); // указатель на void(char*) void f1(char*); // void(char*) int f2(char*); // int(char*) void f3(int*); // void(int*) void f() { pf = &f1; // ok pf = &f2; // ошибка: не подходит возвращаемый тип pf = &f3; // ошибка: не подходит тип параметра (*pf)("asdf"); // ok (*pf)(1); // ошибка: не подходит тип параметра int i = (*pf)("qwer"); // ошибка: void присваивается int'у }
typedef int (*SIG_TYP)(); // из typedef void (*SIG_ARG_TYP); SIG_TYP signal(int,SIG_ARG_TYP);
typedef void (*PF)(); PF edit_ops[] = { // операции редактирования cut, paste, snarf, search }; pf file_ops[] = { // управление файлом open, reshape, close, write };
PF* button2 = edit_ops; pf* button3 = file_ops;
(button2[3])();
typedef int (*CFT)(char*,char*); int sort(char* base, unsigned n, int sz, CFT cmp) /* Сортирует "n" элементов вектора "base" в возрастающем порядке с помощью функции сравнения, указываемой "cmp". Размер элементов "sz". Очень неэффективный алгоритм: пузырьковая сортировка */ { for (int i=0; iname, puser(q)->name); } int cmp2(char*p, char* q) // Сравнивает числа dept { return Puser(p)->dept-Puser(q)->dept; }
main () { sort((char*)heads,6,sizeof(user),cmp1); print_id(heads,6); // в алфавитном порядке cout << "\n"; sort((char*)heads,6,sizeof(user),cmp2); print_id(heads,6); // по порядку подразделений }
Макросы *5 определяются в #с.11. В C они очень важны, но в C++ применяются гораздо меньше. Первое правило относительно них такое:
не используйте их, если вы не обязаны это делать. Как было
замечено, почти каждый макрос проявляет свой изъян или в языке, или
в программе. Если вы хотите использовать макросы, прочитайте,
пожалуйста, вначале очень внимательно руководство по вашей
реализации C препроцессора.
Простой макрос определяется так:
#define name rest of line
named = name
named = rest of line
#define mac(a,b) argument1: a argument2: b
expanded = mac(foo bar, yuk yuk)
expanded = argument1: foo bar argument2: yuk yuk
#define Case break;case #define nl <<"\n" #define forever for(;;) #define MIN(a,b) (((a)<(b))?(a):(b))
#define PI 3.141593 #define BEGIN { #define END }
#define SQUARE(a) a*a #define INCR_xx (xx)++ #define DISP = 4
int xx = 0; // глобальный счетчик void f() { int xx = 0; // локальная переменная xx = square(xx+2); // xx = xx+2*xx+2 incr_xx; // увеличивает локальный xx if (a-disp==b) { // a-= 4==b // ... } }
#define m1(a) something(a) // глубокомысленный комментарий #define m2(a) something(a) /* глубокомысленный комментарий */
int a = m1(1)+2; int b = m2(1)+2;
int a = something(1) // глубокомысленный комментарий+2; int b = something(1) /* глубокомысленный комментарий */+2;
typedef int (rifii&) (int, int);
#define PI = 3.141593 #define max(a,b) a>b?a:b #define fac(a) (a)*fac((a)-1)
*1 или линкер. (прим. перев.)
* 2C разработан так, чтобы в большинстве случаев позволять
осуществлять неявную компоновку. Применение C, однако, возросло
неимоверно, поэтому случаи, когда можно использовать неявную
линковку, сейчас составляют незначительное меньшинство. (прим.
автора)
*3 Легко изменить один компоновщик, но сделав это и написав
программу, которая зависит от усовершенствований, как вы будете
переносить эту программу в другое место? (прим. автора)
*4 Мышь - это указывающее устройство по крайней мере с одной
кнопкой. Моя мышь красная, круглая и с тремя кнопками. (прим.
автора)
*5 часто называемые также макроопределениями. (прим. перев.)