Существует три распространенных способа возврата ошибки из функции:
Пусть функция возвращает int
, с конкретными значениями, указывающими на успех и неудачу
Например, возврат EXIT_SUCCESS
или EXIT_FAILURE
из main()
- это способ, которым стандарты C рекомендуют сообщать об успехе или неудаче всего процесса.(Варианты BSD пытались стандартизировать некоторые другие коды; если ваша система имеет заголовок <sysexits.h>
, вы можете использовать их. Но учтите, что они не являются «стандартными», просто мы ближе всего согласны с тем, как процесс можетсообщить коды ошибок.)
Зарезервируйте конкретное возвращаемое значение для ошибок и используйте глобальную или локальную переменную потока (обычно errno
) для описания ошибки
Большинство стандартных функций библиотеки C делают это с функциями, возвращающими int
с использованием -1
для ошибки, и функциями, возвращающими указатель с использованием NULL
для указания ошибки.
Используйте дополнительный параметр, чтобы указать на индикатор ошибки.
Этот подход является общим для кода и интерфейсов, полученных из Fortran.Часто индикатор ошибки является необязательным, и его можно оставить NULL
, если вызывающему абоненту не интересно, действителен ли результат.
Мои собственные правила просты:
Предпочитайте второй подход при написании низкоуровневой библиотеки.Это знакомый подход для тех, кто знаком со стандартной библиотекой C.
Используйте первый подход для исправляемых ошибок.
Часто я комбинирую его со вторым, используяreturn 0;
для успеха и return errno;
или return errno = EINVAL;
и т. Д. Для ошибок.(Последний сначала назначает EINVAL
для errno
, а затем возвращает EINVAL
.)
Используйте третий подход, когда состояние ошибки должно быть сохранено в течение ряда операций,или существует структура, на которую влияют ошибки состояния.
Давайте посмотрим, как эти методы отличаются на практике.
Очень распространенная вещь, которую нужно сделать, - это анализ командыаргументы в виде чисел.Давайте рассмотрим случай, когда аргументы должны использоваться как double
s, для какого-то рода вычислений:
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int arg;
double val;
for (arg = 1; arg < argc; arg++) {
if (sscanf(argv[arg], "%lf", &val) == 1) {
printf("argv[%d] = %.6f\n", arg, val);
} else {
printf("%s: Not a number.\n", argv[arg]);
exit(EXIT_FAILURE);
}
}
return EXIT_SUCCESS;
}
Выше приведено sscanf()
для преобразования строки.К сожалению, он не проверяет наличие конечного мусора, поэтому он принимает, например, 1.5k
как 1.5
.Чтобы избежать этого, мы можем использовать фиктивный символ для обнаружения завершающего мусора:
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int arg;
double val;
char dummy;
for (arg = 1; arg < argc; arg++) {
if (sscanf(argv[arg], "%lf %c", &val, &dummy) == 1) {
printf("argv[%d] = %.6f\n", arg, val);
} else {
printf("%s: Not a number.\n", argv[arg]);
exit(EXIT_FAILURE);
}
}
return EXIT_SUCCESS;
}
Это работает, потому что sscanf()
возвращает количество успешных преобразований, и мы ожидаем только двойное преобразование (%lf
)к работе, и преобразование символов (%c
) завершится неудачей.
К сожалению, семейство функций scanf не проверяет переполнение.Если вы поставите достаточно большое количество, оно будет молча повреждено.Нехорошо.Чтобы избежать этого, мы можем использовать strtod()
.Чтобы упростить использование, мы можем поместить его в отдельную функцию parse_double()
.Но как это должно вернуть значение и возможную ошибку?Что из нижеперечисленного реализовать?
/* Convert the initial double, returning the pointer to the rest of the
string; or NULL if an error occurs. */
const char *parse_double(const char *src, double *to);
/* If the string contains exactly one double, convert it and return 0.
Otherwise return a nonzero error code. */
int parse_double(const char *src, double *to);
/* Convert the string to a double as best as you can. If an error occurs, return 'errval'. */
double parse_double(const char *src, const double errval);
Итак, какой из них лучший?
Ответ, конечно, зависит от варианта использования .
Я фактически реализовал все три (в отдельных программах), в зависимости от того, какая из них была наиболее подходящей.
Первая особенно полезна, когда одна и та же функцияиспользуется для анализа входных файлов, и / или мы разрешаем любое количество удвоений для параметра / строки.Это очень легко использовать в цикле.
Второй - это то, что я чаще всего использую в программах.Очень часто я использую
typedef struct {
double x;
double y;
double z;
} vec3d;
int parse_vector(const char *src, vec3d *to)
{
vec3d temp;
char dummy;
if (!src || !*src)
return -1; /* NULL or empty string */
if (sscanf(src, " %lf %lf %lf %c", &temp.x, &temp.y, &temp.z, &dummy) == 3 ||
sscanf(src, " %lf %*[.,:/] %lf %*[.,:/] %lf %c", &temp.x, &temp.y, &temp.z, &dummy) == 3) {
if (to)
*to = temp;
return 0;
}
return -1;
}
, что позволяет указывать трехмерный вектор в командной строке, используя 1+2+3
, 1/2/3
, 1:2:3
или даже '1 2 3'
или "1 2 3"
(кавычки необходимы, чтобы помешать оболочке разделить ее на три отдельных аргумента).Он не проверяет переполнение double
, поэтому важно показать проанализированный вектор в выходных данных, чтобы пользователь мог определить, был ли их ввод неверным.
(Звездочка *
в %*[.,:/]
означает, что результат преобразования нигде не хранится, и преобразование не учитывается в возвращаемом значении. [
является спецификатором преобразования, "преобразующим" все и все изсимволы в списке, оканчивающиеся символом ]
. [^
- это инверсия, которая "преобразует" все символы , а не в списке.)