straustrup2 (852740), страница 34
Текст из файла (страница 34)
Функцию реакции наошибку можно определить так:#include <stdarg.h>void error(int severity ...)/*за "severity" (степень тяжести ошибки) следуетсписок строк, завершающийся нулем*/{va_list ap;va_start(ap,severity);// начало параметровfor (;;) {char* p = va_arg(ap,char*);if (p == 0) break;cerr << p << ' ';}va_end(ap);// очистка параметровcerr << '\n';if (severity) exit(severity);}Вначале при вызове va_start() определяется и инициализируется va_list. Параметрамимакроопределения va_start являются имя типа va_list и последний формальный параметр.
Для выборкипо порядку неописанных параметров используется макроопределение va_arg(). В каждом обращении кva_arg нужно задавать тип ожидаемого фактического параметра. В va_arg() предполагается, чтопараметр такого типа присутствует в вызове, но обычно нет возможности проверить это. Передвыходом из функции, в которой было обращение к va_start, необходимо вызвать va_end. Причина в том,что в va_start() могут быть такие операции со стеком, из-за которых корректный возврат из функциистановится невозможным. В va_end() устраняются все нежелательные изменения стека.Приведение 0 к (char*)0 необходимо потому, что sizeof(int) не обязано совпадать с sizeof(char*). Этотпример демонстрирует все те сложности, с которыми приходится сталкиваться программисту, если онрешил обойти контроль типов, используя эллипсис.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")), что являетсяошибкой. По той же причине скобки нужны и при описании указателя на функцию. Однако, писатьпросто efct("error") можно, т.к. транслятор понимает, что efct является указателем на функцию, исоздает команды, делающие вызов нужной функции.Отметим, что формальные параметры в указателях на функцию описываются так же, как и в обычных115Бьерн Страуструп.Язык программирования С++функциях.
При присваивании указателю на функцию требуется точное соответствие типа функции итипа присваиваемого значения. Например:void (*pf)(char*);void f1(char*);int f2(char*);void f3(int*);void f(){pf = &f1;pf = &f2;pf = &f3;(*pf)("asdf");(*pf)(1);int i = (*pf)("qwer");////////указатель на void(char*)void(char*);int(char*);void(int*);//////////////нормальноошибка: не тот тип возвращаемогозначенияошибка: не тот тип параметранормальноошибка: не тот тип параметраошибка: void присваивается int}Правила передачи параметров одинаковы и для обычного вызова, и для вызова с помощью указателя.Часто бывает удобнее обозначить тип указателя на функцию именем, чем все время использоватьдостаточно сложную запись. Например:typedef int (*SIG_TYP)(int);// из <signal.h>typedef void (SIG_ARG_TYP)(int);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;Для настоящей программы редактора надо определить большее число объектов, чтобы описать каждуюпозицию в меню. Например, необходимо где-то хранить строку, задающую текст, который будетвыдаваться для каждой позиции. При работе с системой меню назначение клавиш мыши будет постоянноменяться. Частично эти изменения можно представить как изменения значений указателя, связанного сданной клавишей.
Если пользователь выбрал позицию меню, которая определяется, например, как позиция 3для клавиши 2, то соответствующая команда реализуется вызовом:(*button2[3])();Чтобы полностью оценить мощность конструкции указатель на функцию, стоит попытаться написатьпрограмму без нее. Меню можно изменять в динамике, если добавлять новые функции в таблицукоманд.Довольно просто создавать в динамике и новые меню.Указатели на функции помогают реализовать полиморфические подпрограммы, т.е. такие подпрограммы,которые можно применять к объектам различных типов:typedef int (*CFT)(void*,void*);void sort(void* base, unsigned n, unsigned int sz, CFT cmp)116Бьерн Страуструп.Язык программирования С++/*Сортировка вектора "base" из n элементовв возрастающем порядке;используется функция сравнения, на которую указывает cmp.Размер элементов равен "sz".Алгоритм очень неэффективный: сортировка пузырьковым методом*/{for (int i=0; i<n-1; i++)for (int j=n-1; i<j; j--) {char* pj = (char*)base+j*sz;char* pj1 = pj - sz;if ((*cmp)(pj,pj1) < 0) {// поменять местами b[j] иfor (int k = 0; k<sz; k++)char temp = pj[k];pj[k] = pj1[k];pj1[k] = temp;}}}// b[j]// b[j-1]b[j-1]{}В подпрограмме sort неизвестен тип сортируемых объектов; известно только их число (размермассива), размер каждого элемента и функция, которая может сравнивать объекты.
Мы выбрали дляфункции sort() такой же заголовок, как у qsort() - стандартной функции сортировки из библиотеки С. Этуфункцию используют настоящие программы. Покажем, как с помощью sort() можно отсортироватьтаблицу с такой структурой:struct user {char* name;// имяchar* id;// парольint dept;// отдел};typedef user* Puser;user heads[] = {"Ritchie D.M.","dmr","Sethi R.","ravi","SZYmanski T.G.","tgs","Schryer N.L.","nls","Schryer N.L.","nls","Kernighan B.W.","bwk",};void print_id(Puser v, int n){for (int i=0; i<n; i++)cout << v[i].name << '\t'<< v[i].id << '\t'<< v[i].dept << '\n';}11271,11272,11273,11274,1127511276Чтобы иметь возможность сортировать, нужно вначале определить подходящие функции сравнения.Функция сравнения должна возвращать отрицательное число, если ее первый параметр меньшевторого, нуль, если они равны, и положительное число в противном случае:int cmp1(const void* p, const void* q)// сравнение строк, содержащих имена{return strcmp(Puser(p)->name, Puser(q)->name);}int cmp2(const void* p, const void* q)117Бьерн Страуструп.Язык программирования С++// сравнение номеров разделов{return Puser(p)->dept - Puser(q)->dept;}Следующая программа сортирует и печатает результат:int main(){sort(heads,6,sizeof(user), cmp1);print_id(heads,6);// в алфавитном порядкеcout << "\n";sort(heads,6,sizeof(user),cmp2);print_id(heads,6);// по номерам отделов}Допустима операция взятия адреса и для функции-подстановки, и для перегруженной функции($$R.13.3).Отметим, что неявное преобразование указателя на что-то в указатель типа void* не выполняется дляпараметра функции, вызываемой через указатель на нее.
Поэтому функциюint cmp3(const mytype*, const mytype*);нельзя использовать в качестве параметра для sort(). Поступив иначе, мы нарушаем заданное вописании условие, что cmp3() должна вызываться с параметрами типа mytype*. Если вы специальнохотите нарушить это условие, то должны использовать явное преобразование типа.4.7 МакросредстваМакросредства языка определяются в $$R.16. В С++ они играют гораздо меньшую роль, чем в С.Можно даже дать такой совет: используйте макроопределения только тогда, когда не можете без нихобойтись.
Вообще говоря, считается, что практически каждое появление макроимени являетсясвидетельством некоторых недостатков языка, программы или программиста. Макросредства создаютопределенные трудности для работы служебных системных программ, поскольку они перерабатываютпрограммный текст еще до трансляции. Поэтому, если ваша программа использует макросредства, тосервис, предоставляемый такими программами, как отладчик, профилировщик, программаперекрестных ссылок, будет для нее неполным. Если все-таки вы решите использовать макрокоманды,то вначале тщательно изучите описание препроцессора С++ в вашем справочном руководстве и нестарайтесь быть слишком умным.Простое макроопределение имеет вид:#define имяостаток-строкиВ тексте программы лексема имя заменяется на остаток-строки. Например,объект = имябудет заменено наобъект = остаток-строкиМакроопределение может иметь параметры.
Например:#define mac(a,b)argument1: a argument2: bВ макровызове mac должны быть заданы две строки, представляющие параметры. При подстановкеони заменят a и b в макроопределении mac(). Поэтому строкаexpanded = mac(foo bar, yuk yuk)при подстановке преобразуется вexpanded = argument1: foo bar argument2: yuk yukМакроимена нельзя перегружать. Рекурсивные макровызовы ставят перед препроцессором слишком118Бьерн Страуструп.Язык программирования С++сложную задачу:// ошибка:#define print(a,b) cout<<(a)<<(b)#define print(a,b,c) cout<<(a)<<(b)<<(c)// слишком сложно:#define fac(n) (n>1) ?n*fac(n-1) :1Препроцессор работает со строками и практически ничего не знает о синтаксисе C++, типах языка иобластях видимости.
Транслятор имеет дело только с уже раскрытым макроопределением, поэтомуошибка в нем может диагностироваться уже после подстановки, а не при определении макроимени. Врезультате появляются довольно путанные сообщения об ошибках.Допустимы такие макроопределения:#define Case break;case#define forever for(;;)А вот совершенно излишние макроопределения:#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);INCR_xx;if (a-DISP==b) {// ...}}////////локальная переменнаяxx = xx +2*xx+2;увеличивается локальная переменная xxa-=4==bПри ссылке на глобальные имена в макроопределении используйте операцию разрешения областивидимости ($$2.1.1), и всюду, где это возможно, заключайте имя параметра макроопределения вскобки.