Макро-директивы на C, мой пример кода не работает - PullRequest
2 голосов
/ 31 декабря 2011

Я хочу получить следующий фрагмент кода:

#define READIN(a, b) if(scanf('"#%d"', '"&a"') != 1) { printf("ERROR"); return EXIT_FAILURE; }

int main(void)
{
    unsigned int stack_size;
    printf("Type in size: ");
    READIN(d, stack_size);
}

Не понимаю, как использовать директивы с оператором #. Я хочу использовать scanf с ошибкой печати и т. Д. Несколько раз, но "'"#%d"' & '"&a"'" я считаю совершенно неправильным. Есть ли способ запустить это? Я думаю, что макрос является лучшим решением или нет?

Ответы [ 3 ]

6 голосов
/ 31 декабря 2011

Вы должны только приводить строковые аргументы к макросу, и они должны быть вне строк или символьных констант в тексте замены макроса. Таким образом, вы, вероятно, должны использовать:

#define READIN(a, b) do { if (scanf("%" #a, &b) != 1) \
                          { fprintf(stderr, "ERROR\n"); return EXIT_FAILURE; } \
                     } while (0)

int main(void)
{
    unsigned int stack_size;
    printf("Type in size: ");
    READIN(u, stack_size);
    printf("You entered %u\n", stack_size);
    return(0);
}

Есть много изменений. Идиома do { ... } while (0) предотвращает получение ошибок компиляции при таких обстоятельствах, как:

if (i > 10)
    READIN(u, j);
else
    READIN(u, k);

С вашим макросом вы получите сообщение типа unexpected keyword 'else', потому что точка с запятой после первого READIN() будет пустой инструкцией после встроенного if, поэтому else не может принадлежать видимый if или if внутри макроса.

Тип stack_size равен unsigned int; следовательно, правильный спецификатор формата - u (d для подписи int).

И, что самое важное, аргумент a в макросе правильно структурирован (и конкатенация строк смежных строковых литералов - чрезвычайно полезная особенность C89! - позаботится обо всем остальном. И аргумент b в макрос тоже не встраивается в строку.

Отчет об ошибках передается в stderr (стандартный поток сообщений об ошибках), и сообщение заканчивается новой строкой, поэтому оно действительно появится. Я не заменил return EXIT_FAILURE; на exit(EXIT_FAILURE);, но это, вероятно, будет разумным выбором, если макрос будет использоваться за пределами main(). Это предполагает, что «прекращение при ошибке» - это, в первую очередь, правильное поведение. Часто это не для интерактивных программ, но исправить это немного сложнее.

Я также игнорирую свои оговорки по поводу использования scanf() вообще; Я обычно избегаю этого, потому что нахожу восстановление после ошибок слишком сложным. Я программировал на C всего около 28 лет, и мне все еще трудно scanf() контролировать, поэтому я по сути никогда не использую его. Я обычно использую fgets() и sscanf() вместо этого. Среди других достоинств я могу сообщить о строке, которая вызвала проблему; это трудно сделать, когда scanf(), возможно, сожрал некоторые из них.


Моя мысль с scanf() заключается в том, чтобы читать только в положительных числах и без букв. Мой общий код создает стек, который пользователь вводит, и тип должен быть только положительным, в противном случае ошибка. [...] Я только хотел узнать, есть ли лучшее решение, запрещающее пользователю вводить что-то кроме положительных чисел?

Я только что попробовал приведенный выше код (с добавлением #include <stdlib.h> и #include <stdio.h>), ввел -2 и получил 4294967294, что не то, что я хотел (формат %u не отклоняет -2, по крайней мере на MacOS X 10.7.2). Итак, я бы пошел с fgets() и strtoul(), скорее всего. Тем не менее, точное обнаружение всех возможных проблем с strtoul() является проявлением некоторой деликатности.

Это альтернативный код, который я придумал:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
#include <string.h>

int main(void)
{
    unsigned int stack_size = 0;
    char buffer[4096];
    printf("Type in size: ");
    if (fgets(buffer, sizeof(buffer), stdin) == 0)
        printf("EOF or error detected\n");
    else
    {
        char *eos;
        unsigned long u;
        size_t len = strlen(buffer);
        if (len > 0)
            buffer[len - 1] = '\0';  // Zap newline (assuming there is one)
        errno = 0;
        u = strtoul(buffer, &eos, 10);
        if (eos == buffer ||
            (u == 0 && errno != 0) ||
            (u == ULONG_MAX && errno != 0) ||
            (u > UINT_MAX))
        {
            printf("Oops: one of many problems occurred converting <<%s>> to unsigned integer\n", buffer);
        }
        else
            stack_size = u;
        printf("You entered %u\n", stack_size);
    }
    return(0);
}

Спецификация strtoul() приведена в ISO / IEC 9899: 1999 § 7.20.1.4:

¶1 [...]

unsigned long int strtoul(const char * restrict nptr,<br> char ** restrict endptr, int base);

[...]

№2 [...] Во-первых, они разбивают входную строку на три части: начальную, возможно пустую, последовательность символы пробела (как указано в функции isspace), последовательность субъекта напоминающие целое число, представленное в некотором радиксе, определяемом значением основания, и конечная строка из одного или нескольких нераспознанных символов, включая завершающий ноль символ входной строки. Затем они пытаются преобразовать последовательность объекта в целое число и возвращает результат.

¶3 [...]

¶4 Предметная последовательность определяется как самая длинная начальная подпоследовательность входной строки, начиная с первого символа, не являющегося пробелом, который имеет ожидаемую форму. Предмет последовательность не содержит символов, если входная строка пуста или полностью состоит из белого пробел, или если первый символ, не являющийся пробелом, отличается от знака или допустимой буквы или цифра.

¶5 Если последовательность объекта имеет ожидаемую форму, а значение базы равно нулю, последовательность символов, начинающихся с первой цифры, интерпретируется как целочисленная константа в соответствии с правила 6.4.4.1. Если предметная последовательность имеет ожидаемую форму и значение базы находится между 2 и 36, он используется в качестве основы для преобразования, приписывая каждой букве свое значение как указано выше. Если предметная последовательность начинается со знака минус, значение, полученное из преобразование отменяется (в типе возврата). Указатель на последнюю строку хранится в объект, на который указывает endptr, при условии, что endptr не является нулевым указателем.

¶6 [...]

¶7 Если последовательность объекта пуста или не имеет ожидаемой формы, преобразование не выполняется. выполнила; значение nptr сохраняется в объекте, указанном endptr, при условии что endptr не является нулевым указателем.

Возвращает

¶8 Функции strtol, strtoll, strtoul и strtoull возвращают преобразованные значение, если есть. Если преобразование не может быть выполнено, возвращается ноль. Если правильное значение находится вне диапазона представимых значений, LONG_MIN, LONG_MAX, LLONG_MIN, LLONG_MAX, ULONG_MAX или ULLONG_MAX возвращается (в соответствии с типом возврата и знак значения, если оно есть), а значение макроса ERANGE хранится в errno.

Ошибка, которую я получил от 64-битной компиляции, где -2 был преобразован в 64-битную длину без знака, и это было вне диапазона, приемлемого для 32-битной unsigned int (условие сбоя было u > UINT_MAX). Когда я перекомпилировал в 32-битном режиме (т. Е. sizeof(unsigned int) == sizeof(unsigned long)), значение -2 снова было принято, снова интерпретировано как 4294967294. Таким образом, даже это не является достаточно деликатным ... вам, вероятно, придется вручную пропустить начальные пробелы и отклонить отрицательный знак (и, возможно, тоже положительный знак; вам также потребуется #include <ctype.h>):

        char *bos = buffer;
        while (isspace(*bos))
            bos++;
        if (!isdigit(*bos))
            ...error - not a digit...
        char *eos;
        unsigned long u;
        size_t len = strlen(bos);
        if (len > 0)
            bos[len - 1] = '\0';  // Zap newline (assuming there is one)
        errno = 0;
        u = strtoul(bos, &eos, 10);
        if (eos == bos ||
            (u == 0 && errno != 0) ||
            (u == ULONG_MAX && errno != 0) ||
            (u > UINT_MAX))
        {
            printf("Oops: one of many problems occurred converting <<%s>> to unsigned integer\n", buffer);
        }

Как я уже сказал, весь процесс довольно нетривиален.

( Глядя на это снова, я не уверен, что предложение u == 0 && errno != 0 когда-либо будет ловить какие-либо ошибки ... возможно, не потому, что условие eos == buffer (или eos == bos) ловит случай, когда ничего нет конвертировать вообще. )

4 голосов
/ 31 декабря 2011

Вы неправильно упаковываете свой аргумент (ы) макроса, он должен выглядеть следующим образом:

#define READIN(a, b) if(scanf("%"#a, &b) != 1) { printf("ERROR"); return EXIT_FAILURE; }

вы использовали оператор stringify также неверно, он должен непосредственно префикс имени аргумента.

Короче говоря, используйте "%"#a, а не '"#%d"' и &b, а не '"&a"'.

В качестве дополнительного примечания, для длинных макросов, таких как те, они помогают сделать их многострочными, используя \, это делает их читаемыми:

#define READIN(a, b) \
if(scanf("%"#a, &b) != 1) \
{ \
    printf("ERROR"); \
    return EXIT_FAILURE; \
}

При выполнении чего-то подобного предпочтительно использовать функцию, что-то вроде этого должно работать:

inline int readIn(char* szFormat, void* pDst)
{
    if(scanf(szFormat,pDst) != 1)
    {
        puts("Error");
        return 0;
    }

    return 1;
}

вызывая это было бы так:

if(!readIn("%d",&stack_size))
    return EXIT_FAILURE;
3 голосов
/ 31 декабря 2011

scanf(3) принимает const char * в качестве первого аргумента.Вы передаете '"..."', который не является C-строкой.Строки C пишутся с двойными кавычками ".' одинарные кавычки предназначены для отдельных символов : 'a' или '\n' и т. Д.

Размещение оператора return внутри макроса препроцессора C обычно считается очень плохой формой.Я видел goto error;, закодированный внутри макросов препроцессора, для повторяющегося кода обработки ошибок при сохранении отформатированных данных и чтении данных из файла или интерфейса ядра, но это определенно исключительные обстоятельства.Вы бы не захотели отладить это через шесть месяцев.Доверьтесь мне.Не скрывайте goto, return, break, continue внутри макросов препроцессора C.if в порядке, если он целиком содержится в макросе.

Кроме того, вы должны иметь привычку писать printf(3) утверждения вроде этого:

printf("%s", "ERROR");

Уязвимости форматной строки чрезвычайно легко написать.В вашем коде сейчас нет такой уязвимости, но, поверьте мне, на некотором этапе в будущем эти строки неизбежно изменяются, чтобы включать в себя некоторый предоставленный пользователем контент, и добавление в явную строку формата теперь поможет предотвратить это в будущем.По крайней мере, вы подумаете об этом в будущем, если увидите это.

считается вежливым упаковывать ваши многострочные макросы в do { } while (0) блоки .

Наконец, stringification не совсем правильно;попробуйте вместо этого:

#define READIN(A, B) do { if (scanf("%" #A, B) != 1) { \
        /* error handling */ \
    } else { \
        /* success case */ \
    } } while(0)

Редактировать : я чувствую, что должен повторить совет akappa : Вместо этого используйте функцию.Вы получаете лучшую проверку типов, лучшие обратные трассировки, когда что-то идет не так, и с ним гораздо проще работать.Функции хорошие.

...