|
|
|||
|
wm-help.net -> Электронная библиотека -> C++/C#/C -> Бьярн Страустрап. Введение в язык Си++ -> Выражения и операторыВыражения и операторы
Глава 4Функции и ФайлыИтерация свойственна человеку, рекурсия божественна. - Л. Питер Дойч Все нетривиальные программы собираются из нескольких раздельно компилируемых единиц (их принято называть просто файлами). В этой главе описано, как раздельно откомпилированные функции могут обращаться друг к другу, как такие функции могут совместно пользоваться данными (разделять данные), и как можно обеспечить согласованность типов, которые используются в разных файлах программы. Функции обсуждаются довольно подробно. Сюда входят передача параметров, параметры по умолчанию, перегрузка имен функций, и, конечно же, описание и определение функций. В конце описываются макросы. 4.1 ВведениеИметь всю программу в одном файле обычно
невозможно, поскольку коды стандартных
библиотек и операционной системы находятся
где-то в другом месте. Кроме того, хранить весь
текст пользовательской программы в одном файле
как правило непрактично и неудобно. Способ
организации программы в файлы может помочь
читающему охватить всю структуру программы, а
также может дать возможность компилятору
реализовать эту структуру. Поскольку единицей
компиляции является файл, то во всех случаях,
когда в файл вносится изменение (сколь бы мало
оно ни было), весь файл нужно компилировать
заново. Даже для программы умеренных размеров
время, затрачиваемое на перекомпиляцию, можно
значительно снизить с помощью разбиения
программы на файлы подходящих размеров. Программист может скомпенсировать недостаток поддержки со стороны компоновщика, предоставив дополнительную информацию о типах (описания). После этого согласованность программы обеспечивается проверкой согласованности описаний, которые находятся в отдельно компилируемых частях. Средства, которые это обеспечивают, в вашей системе будут. C++ разработан так, чтобы способствовать такой явной компоновке*2. 4.2 КомпоновкаЕсли не указано иное, то имя, не являющееся локальным для функции или класса, в каждой части программы, компилируемой отдельно, должно относиться к одному и тому же типу, значению, функции или объекту. То есть, в программе может быть только один нелокальный тип, значение, функция или объект с этим именем. Рассмотрим, например, два файла:
// file1.c:
int a = 1;
int f() { /* что-то делает */ }
// file2.c:
extern int a;
int f();
void g() { a = f(); }
// file1.c:
int a = 1;
int b = 1;
extern int c;
// file2.c:
int a;
extern double b;
extern int c;
// file1.c:
int a;
int f() { return a; }
// file2.c:
int a;
int g() { return f(); }
// file1.c:
static int a = 6;
static int f() { /* ... */ }
// file2.c:
static int a = 7;
static int f() { /* ... */ }
// 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; }
4.3 Заголовочные Файлы
Типы во всех описаниях одного и того же объекта
должны быть согласованными. Один из способов это
достичь мог бы состоять в обеспечении средств
проверки типов в компоновщике, но большинство
компоновщиков - образца 1950-х, и их нельзя изменить
по практическим соображениям *3.
Другой подход состоит в обеспечении того, что
исходный текст, как он передается на
рассмотрение компилятору, или согласован, или
содержит информацию, которая позволяет
компилятору обнаружить несогласованности. Один
несовершенный, но простой способ достичь
согласованности состоит во включении
заголовочных файлов, содержащих интерфейсную
информацию, в исходные файлы, в которых
содержится исполняемый код и/или определения
данных. #include "to_be_included"
CC -E file.c
#include // из стандартной директории включения #define "myheader.h" // из текущей директории
#include
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: ввод и лексический анализ
#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[]) { /* ... */ }
4.3.2 Множественные Заголовочные ФайлыСтиль разбиения программы с одним заголовочным
файлом наиболее пригоден в тех случаях, когда
программа невелика и ее части не предполагается
использовать отдельно. Поэтому то, что
невозможно установить, какие описания зачем
помещены в заголовочный файл, несущественно.
Помочь могут комментарии. Другой способ - сделать
так, чтобы каждая часть программы имела свой
заголовочный файл, в котором определяются
предоставляемые этой частью средства. Тогда
каждый .c файл имеет соответствующий .h файл, и
каждый .c файл включает свой собственный
(специфицирующий то, что в нем задается) .h файл и,
возможно, некоторые другие .h файлы
(специфицирующие то, что ему нужно).
// 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[]) { /* ... */ }
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); }
// table.c: определения таблицы имен
#include "error.h"
#include
#include "table.h"
const TBLSZ = 23;
static name* table[TBLSZ];
name* look(char* p; int ins) { /* ... */ }
4.4 Файлы как МодулиВ предыдущем разделе .c и .h файлы вместе
определяли часть программы. Файл .h является
интерфейсом, который используют другие части
программы; .c файл задает реализацию. Такой объект
часто называют модулем. Доступными делаются
только те имена, которые необходимо знать
пользователю, остальные скрыты. Это качество
часто называют скрытием данных, хотя данные -
лишь часть того, что может быть скрыто. Модули
такого вида обеспечивают большую гибкость.
Например, реализация может состоять из одного
или более .c файлов, и в виде .h файлов может быть
предоставлено несколько интерфейсов.
Информация, которую пользователю знать не
обязательно, искусно скрыта в .c файлах. Если
важно, что пользователь не должен точно знать,
что содержится в .c файлах, не надо делать их
доступными в исходом виде. Достаточно
эквивалентных им выходных файлов компилятора (.o
файлов). 4.5 Как Создать БиблиотекуФразы типа "помещен в библиотеку" и
"ищется в какой-то библиотеке" используются
часто (и в этой книге, и в других), но что это
означает для C++ программы? К сожалению, ответ
зависит от того, какая операционная система
используется; в этом разделе объясняется, как
создать библиотеку в 8-ой версии системы UNIX.
Другие системы предоставляют аналогичные
возможности. extern double sqrt(double); // подмножество extern double sin(double); extern double cos(double); extern double exp(double); extern double log(double);
$ 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
$ CC myprog.c math.a
$ CC myprog.c sqrt.o sin.o cos.o exp.o log.o
$ CC myprog.c sqrt.o cos.o
4.6 ФункцииОбычный способ сделать что-либо в C++ программе - это вызвать функцию, которая это делает. Определение функции является способом задать то, как должно делаться некоторое действие. Функция не может быть вызвана, пока она не описана. 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);
4.6.2 Определения ФункцийКаждая функция, вызываемая в программе, должна быть где-то определена (только один раз). Определение функции - это описание функции, в котором приводится тело функции. Например:
extern void swap(int*, int*); // описание
void swap(int*, int*) // определение
{
int t = *p;
*p =*q;
*q = t;
}
4.6.3 Передача ПараметровКогда вызывается функция, дополнительно выделяется память под ее формальные параметры, и каждый формальный параметр инициализируется соответствующим ему фактическим параметром. Семантика передачи параметров идентична семантике инициализации. В частности, тип фактического параметра сопоставляется с типом формального параметра, и выполняются все стандартные и определенные пользователем преобразования типов. Есть особые правила для передачи векторов (#4.6.5), средство передавать параметр без проверки (#4.6.8) и средство для задания параметров по умолчанию (#4.6.6). Рассмотрим
void f(int val, int& ref)
{
val++;
ref++;
}
int i = 1; int j = 1; f(i,j);
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*);
4.6.4 Возврат ЗначенияИз функции, которая не описана как 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;} // так не делайте
4.6.5 Векторные ПараметрыЕсли в качестве параметра функции используется вектор, то передается указатель на его первый элемент. Например:
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"; } }
Матрица, конечно, все равно передается как указатель, а
размерности используются просто для удобства записи.
Первая размерность массива не имеет отношения к задаче отыскания
положения элемента (#2.3.6). Поэтому ее можно передавать как параметр:
void print_mi4(int m[][4], int dim1)
{
for (int i = 0; i
4.6.6 Параметры по УмолчаниюЧасто в самом общем случае функции требуется больше параметров, чем в самом простом и более употребительном случае. Например, в библиотеке потоков есть функция 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); // синтаксическая ошибка 4.6.7 Перегрузка Имен ФункцийКак правило, давать разным функциям разные имена - мысль хорошая, но когда некоторые функции выполняют одинаковую работу над объектами разных типов, может быть более удобно дать им одно и то же имя. Использование одного имени для различных действий над различными типами называется перегрузкой (overloading). Метод уже используется для основных операций C++: у сложения существует только одно имя, +, но его можно применять для сложения значений целых, плавающих и указательных типов. Эта идея легко расширяется на обработку операций, определенных пользователем, то есть, функций. Чтобы уберечь программиста от случайного повторного использования имени, имя может использоваться более чем для одной функции только если оно сперва описано как перегруженное. Например: overload print; void print(int); void print(char*); Что касается компилятора, единственное общее, что имеют функции с одинаковым именем, это имя. Предположительно, они в каком-то смысле похожи, но в этом язык ни стесняет программиста, ни помогает ему. Таким образом, перегруженные имена функций - это главным образом удобство записи. Это удобство значительно в случае функций с общепринятыми именами вроде sqrt, print и open. Когда имя семантически значимо, как это имеет место для операций вроде +, * и << (#6.2) и в случае конструкторов (#5.2.4 и #6.3.1), это удобство становится существенным. Когда вызывается перегруженная f(), компилятор должен понять, к какой из функций с именем f следует обратиться. Это делается путем сравнения типов фактических параметров с типами формальных параметров всех функций с именем f. Поиск функции, которую надо вызвать, осуществляется за три отдельных шага: [1] Искать функцию соответствующую точно, и использовать ее, если она найдена; [2] Искать соответствующую функцию используя встроенные преобразования и использовать любую найденную функцию; и [3] Искать соответствующую функцию используя преобразования, определенные пользователем (#6.3), и если множество преобразований единственно, использовать найденную функцию. Например:
overload print(double), print(int);
void f();
{
print(1);
print(1.0);
}
Правило точного соответствия гарантирует, что f напечатает 1 как целое и 1.0 как число с плавающей точкой. Ноль, char или short точно соответствуют параметру int. Аналогично, float точно соответствует double. К параметрам функций с перегруженными именами стандартные C++ правила преобразования (#с.6.6) применяются не полностью. Преобразования, могущие уничтожить информацию, не выполняются. Остаются int в long, int в double, ноль в long, ноль в double и преобразования указателей: ноль в указатель, ноль в void*, и указатель на производный класс в указатель на базовый класс (#7.2.4). Вот пример, в котором преобразование необходимо:
overload print(double), print(long);
void f(int a);
{
print(a);
}
Здесь a может быть напечатано или как double, или как long. Неоднозначность разрешается явным преобразованием типа (или print(long(a)) или print(double(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); Процесс поиска подходящей функции игнорирует unsigned и const. 4.6.8 Незаданное Число ПараметровДля некоторых функций невозможно задать число и тип всех параметров, которые можно ожидать в вызове. Такую функцию описывают завершая список описаний параметров многоточием (...), что означает "и может быть, еще какие-то параметры". Например: int printf(char* ...); Это задает, что в вызове printf должен быть по меньшей мере один параметр, char*, а остальные могут быть, а могут и не быть. Например:
printf("Hello, world\n");
printf("Мое имя %s %s\n", first_name, second_name);
printf("%d + %d = %d\n",2,3,5);
Такая функция полагается на информацию, которая недоступна компилятору при интерпретации ее списка параметров. В случае printf() первым параметром является строка формата, содержащая специальные последовательности символов, позволяющие printf() правильно обрабатывать остальные параметры. %s означает "жди параметра char*", а %d означает "жди параметра int". Однако, компилятор этого не знает, поэтому он не может убедиться в том, что ожидаемые параметры имеют соответствующий тип. Например:
printf("Мое имя %s %s\n",2);
откомпилируется и в лучшем случае приведет к какой-нибудь странного вида выдаче. Очевидно, если параметр не был описан, то у компилятора нет информации, необходимой для выполнения над ним проверки типа и преобразования типа. В этом случае char или short передаются как int, а float передается как double. Это не обязательно то, чего ждет пользователь. Чрезмерное использование многоточий, вроде wild(...), полностью выключает проверку типов параметров, оставляя программиста открытым перед множеством неприятностей, которые хорошо знакомы программистам на C. В хорошо продуманной программе требуется самое большее несколько функций, для которых типы параметров не определены полностью. Для того, чтобы позаботиться о проверке типов, можно использовать перегруженные функции и функции с параметрами по умолчанию в большинстве тех случаев, когда иначе пришлось бы оставить типы параметров незаданными. Многоточие необходимо только если изменяются и число параметров, и тип параметров. Наиболее обычное применение многоточия в задании интерфейса с функциями C библиотек, которые были определены в то время, когда альтернативы не было: 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); }
Первый из va_list определяется и инициализируется вызовом
va_start(). Макрос va_start получает имя va_list'а и имя последнего
формального параметра как параметры. Макрос va_arg используется для
выбора неименованных параметров по порядку. При каждом обращении
программист должен задать тип; va_arg() предполагает, что был
передан фактический параметр, но обычно способа убедиться в этом
нет. Перед возвратом из функции, в которой был использован
va_start(), должен быть вызван va_end(). Причина в том, что
va_start() может изменить стек так, что нельзя будет успешно
осуществить возврат; va_end() аннулирует все эти изменения.
4.6.9 Указатель на ФункциюС функцией можно делать только две вещи: вызывать ее и брать ее адрес. Указатель, полученный взятием адреса функции, можно затем использовать для вызова этой функции. Например:
void error(char* p) { /* ... */ }
void (*efct)(char*); // указатель на функцию
void f()
{
efct = &error; // efct указывает на error
(*efct)("error"); // вызов error через efct
}
Чтобы вызвать функцию через указатель, например, efct, надо сначала
этот указатель разыменовать, *efct. Поскольку операция вызова
функции () имеет более высокий приоритет, чем операция
разыменования *, то нельзя писать просто *efct("error"). Это
означает *efct("error"), а это ошибка в типе. То же относится и к
синтаксису описаний (см. также #7.3.4).
Заметьте, что у указателей на функции типы параметров описываются
точно также, как и в самих функциях. В присваиваниях указателя
должно соблюдаться точное соответствие полного типа функции.
Например:
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); Бывает часто полезен вектор указателей на функцию. Например, система меню для моего редактора с мышью*4 реализована с помощью векторов указателей на функции для представления действий. Подробно эту систему здесь описать не получится, но вот общая идея:
typedef void (*PF)();
PF edit_ops[] = { // операции редактирования
cut, paste, snarf, search
};
PF file_ops[] = { // управление файлом
open, reshape, close, write
};
Затем определяем и инициализируем указатели, определяющие действия, выбранные в меню, которое связано с кнопками (button) мыши: PF* button2 = edit_ops; PF* button3 = file_ops; В полной реализации для определения каждого пункта меню требуется больше информации. Например, где-то должна храниться строка, задающая текст, который высвечивается. При использовании системы значение кнопок мыши часто меняется в зависимости от ситуации. Эти изменения осуществляются (частично) посредством смены значений указателей кнопок. Когда пользователь выбирает пункт меню, например пункт 3 для кнопки 2, выполняется связанное с ним действие: (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); // по порядку подразделений }
Можно взять адрес inline-функции, как, впрочем, и адрес
перегруженной функции(#с.8.9).
4.7 МакросыМакросы *5 определяются в #с.11. В C они очень важны, но в C++ применяются гораздо меньше. Первое правило относительно них такое: не используйте их, если вы не обязаны это делать. Как было замечено, почти каждый макрос проявляет свой изъян или в языке, или в программе. Если вы хотите использовать макросы, прочитайте, пожалуйста, вначале очень внимательно руководство по вашей реализации C препроцессора.Простой макрос определяется так: #define name rest of line Когда name встречается как лексема, оно заменяется на rest of line. Например: named = name после расширения даст: named = rest of line Можно также определить макрос с параметрами. Например: #define mac(a,b) argument1: a argument2: b При использовании mac должно даваться две строки параметра. После расширения mac() они заменяют a и b. Например: expanded = mac(foo bar, yuk yuk) после расширения даст expanded = argument1: foo bar argument2: yuk yuk Макросы обрабатывают строки и о синтаксисе C++ знают очень мало, а о типах C++ или областях видимости - ничего. Компилятор видит только расширенную форму макроса, поэтому ошибка в макросе диагностируется когда макрос расширен, а не когда он определен. В результате этого возникают непонятные сообщения об ошибках. Вот такими макросы могут быть вполне: #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
// ...
}
}
Если вы вынуждены использовать макрос, при ссылке на глобальные имена используйте операцию разрешения области видимости :: (#2.1.1) и заключайте вхождения имени параметра макроса в скобки везде, где это возможно (см. MIN выше). Обратите внимание на различие результатов расширения этих двух макросов: #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; С помощью макросов вы можете разработать свой собственный язык. Скорее всего, для всех остальных он будет непостижим. Кроме того, C препроцессор - очень простой макропроцессор. Когда вы попытаетесь сделать что-либо нетривиальное, вы, вероятно, обнаружите, что сделать это либо невозможно, либо чрезвычайно трудно (но см. #7.3.5). 4.8 Упражнения
*1 или линкер. (прим. перев.) [Назад] [Содержание] [Вперед] |