На мову C подеколи нарікають за синтаксис її оголошень, особливо тих, що стосуються покажчиків на функції. Цей синтаксис являється спробою узгодити оголошення із використанням; він добре працює в простих випадках, але може заплутати в складніших, так як оголошення не можна читати з права на ліво і часом дужок занадто багато. Різниця між
int *f(); /* f: функція, що повертає покажчик на int */
і
int (*pf)(); /* pf: покажчик на функцію, що повертає int */
яскраво ілюструє цю проблему: * — це префіксний оператор і має нижчий пріоритет за (), тож дужки необхідні, щоб забезпечити відповідний зв'язок.
Хоча дійсно складні оголошення рідко зустрічаються на практиці, важливо знати як їх зрозуміти і, коли треба, як створити їх. Один з непоганих шляхів синтезувати оголошення, це за допомогою невеличких кроків із typedef, розглянутого в Розділі 6.7. Як альтернатива, в цьому розділі ми представимо пару програм, що перекладають з чинної C на людську мову, а потім назад. Словесний опис можна прочитати зліва направо.
Перша, dcl, дещо складніша. Вона перекладає оголошення C у слова, як, скажімо,
char **argv
argv: pointer to char
int (*daytab)[13]
daytab: pointer to array[13] of int
int *daytab[13]
daytab: array[13] of pointer to int
void *comp()
comp: function returning pointer to void
void (*comp)()
comp: pointer to function returning void
char (*(*x())[])()
x: function returning pointer to array[] of
pointer to function returning char
char (*(*x[3])())[5]
x: array[3] of pointer to function returning
pointer to array[5] of char
dcl основано на граматиці оголошень, яку точно описано в Додатку А, Розділі 8.5; ось спрощена форма:
оголошувач:
| необов'язкові * прямий-оголошувач
|
прямий-оголошувач:
| назва
|
| (оголошувач)
|
прямий-оголошувач()
| |
| прямий-оголошувач[необов'язковий розмір]
|
тобто, оголошувач — це прямий-оголошувач, можливо із попередніми *. прямий-оголошувач — це або назва, або внесений в дужки оголошувач, або прямий-оголошувач із наступними дужками, або прямий-оголошувач з квадратними дужками і необов'язковим розміром.
Ця граматика може бути використана для аналізу оголошень. Наприклад, розглянемо оголошення
(*pfa[])()
де pfa буде ідентифіковано як назву, а тому як прямий-оголошувач. Тоді pfa[] — це такожпрямий-оголошувач. Після цього *pfa[] розпізнано як оголошувач, тож (*pfa[]) є прямим-оголошувачем. Потім (*pfa[])() розпізнано як прямий-оголошувач, а тому як оголошувач. Ми можемо зобразити цей аналіз графічно як дерево (прямий-оголошувач скорочено до пр-ог):
( * pfa [] ) ()
\ \ | / / /
\ \ назва / / /
\ \ | / / /
\ \ пр-ог / / /
\ \ | / / /
\ \ пр-ог / /
\ \ | / /
\ ог / /
\ | / /
пр-ог /
| /
пр-ог
|
ог
Основою програми dcl є пара функцій — dcl і dirdcl — котрі обробляють оголошення відповідно до цієї граматики. Оскільки граматика означена рекурсивно, функції викликають одна одну рекурсивно по мірі того, як вони розпізнають частини оголошення; програма називається рекурсивно-спадним оброблювачем.
/* dcl: прочитує оголошувач */
void dcl(void)
{
int ns;
for (ns = 0; gettoken() == '*'; ) /* count *'s */
ns++;
dirdcl();
while (ns-- > 0)
strcat(out, " pointer to");
}
/* dirdcl: прочитує безпосередній оголошувач */
void dirdcl(void)
{
int type;
if (tokentype == '(') { /* ( dcl ) */
dcl();
if (tokentype != ')')
printf("error: missing )\n");
} else if (tokentype == NAME) /* variable name */
strcpy(name, token);
else
printf("error: expected name or (dcl)\n");
while ((type=gettoken()) == PARENS || type == BRACKETS)
if (type == PARENS)
strcat(out, " function returning");
else {
strcat(out, " array");
strcat(out, token);
strcat(out, " of");
}
}
Ми хотіли, щоб програма була ілюстративною, а не куленепробивною, тому dcl багато в чому обмежена. Вона розуміє записи лише простих типів даних, таких як char або int. Вона не розуміє типи аргументів функцій або класифікатори на кшталт const. Випадкові пробіли заплутують її. Вона не здійснює якихось спроб обробки помилок, тож неправильні оголошення також заплутають її. Покращення цієї програми ми залишаємо вам як вправа.
Ось глобальні змінні і функція main:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define MAXTOKEN 100
enum { NAME, PARENS, BRACKETS };
void dcl(void);
void dirdcl(void);
int gettoken(void);
int tokentype; /* тип останньої лексеми */
char token[MAXTOKEN]; /* ланцюжок останньої лексеми */
char name[MAXTOKEN]; /* назва ідентифікатору */
char datatype[MAXTOKEN]; /* тип даних = char, int тощо */
char out[1000];
main() /* перетворює оголошення на словесний опис */
{
while (gettoken() != EOF) { /* перша лексема рядка */
strcpy(datatype, token); /* is the datatype */
out[0] = '\0';
dcl(); /* читання решти рядка */
if (tokentype != '\n')
printf("syntax error\n");
printf("%s: %s %s\n", name, out, datatype);
}
return 0;
}
Функція gettoken опускає пробіли і табуляцію, після чого знаходить наступну лексему у вводі; лексемою може бути ім'я, пара дужок, пара квадратних дужок із можливим числом всередині, або будь-який інший одиничний знак.
int gettoken(void) /* return next token */
{
int c, getch(void);
void ungetch(int);
char *p = token;
while ((c = getch()) == ' ' || c == '\t')
;
if (c == '(') {
if ((c = getch()) == ')') {
strcpy(token, "()");
return tokentype = PARENS;
} else {
ungetch(c);
return tokentype = '(';
}
} else if (c == '[') {
for (*p++ = c; (*p++ = getch()) != ']'; )
;
*p = '\0';
return tokentype = BRACKETS;
} else if (isalpha(c)) {
for (*p++ = c; isalnum(c = getch()); )
*p++ = c;
*p = '\0';
ungetch(c);
return tokentype = NAME;
} else
return tokentype = c;
}
getch і ungetch обговорювались у Розділі 4.
Протилежний напрямок є легшим, особливо якщо ми не хвилюватимемось про створення зайвих дужок. Програма undcl перетворює словесний опис на зразок «x is a function returning a pointer to an array of pointers to functions returning char», який ми виразимо як
x () * [] * () char
на
char (*(*x())[])()
Скорочений синтаксис вводу дозволяє нам повторне використання функції gettoken. undclтакож використовує ті самі зовнішні змінні що й dcl.
/* undcl: перетворює словесний опис на оголошення */
main()
{
int type;
char temp[MAXTOKEN];
while (gettoken() != EOF) {
strcpy(out, token);
while ((type = gettoken()) != '\n')
if (type == PARENS || type == BRACKETS)
strcat(out, token);
else if (type == '*') {
sprintf(temp, "(*%s)", out);
strcpy(out, temp);
} else if (type == NAME) {
sprintf(temp, "%s %s", token, out);
strcpy(out, temp);
} else
printf("invalid input at %s\n", token);
}
return 0;
}
Вправа 5-18. Додайте перевірку на помилки і відновлення після помилок до dcl.
Вправа 5-19. Змініть undcl таким чином, щоб вона не додавала зайві дужки до оголошень.
Вправа 5-20. Розширте dcl, щоб вона обробляла оголошення з типами аргументів функцій, класифікатори на зразок const тощо.