从函数返回多个错误之一并用C结束程序的最佳实践是什么?

时间:2018-12-19 22:12:09

标签: c

我找不到从函数内部的函数返回错误并结束程序的方法。

我当时正在考虑使用一种结构并将返回值存储在内部并以这种方式结束它,但是我不知道这是否是最佳实践

让我们说我有一个这样的程序:

int main()
{
  // do stuff 
  importantFunction();
  // do stuff
  return 0;
}

在ImportantFunction()中,我正在调用其他两个函数,这些函数在进行某些Bitshifting并始终返回一个数组。现在,如果该函数之一发生错误,我想尝试返回1(或0x01,因为该函数正在返回数组指针),但是我不确定如何。

char *importantFunction()
{
  //do stuff
  secoundFunction();
  thirdFunction();
  //do stuff
  return array;
 }


char *secoundFunction()
{
  // do stuff
  if (something == x)
    return array;
  // do stuff
  return array;
}

我只是想找到一个方法,而不必检查第一个函数是否等于某个方法,然后在int main中结束程序。 我试图避免这种情况,因为它并不总是有效:

int main()
{
  // do stuff 
  char *pointer = importantFunction();
  if (*pointer == 'something')
    return 1;
  if (*pointer == 'something')
    return 2;
  if (*pointer == 'something')
    return 3;
  // and so on...
  // do stuff
  return 0;
}

如果这是一个愚蠢的问题,我很抱歉,我不太会问问题。

5 个答案:

答案 0 :(得分:4)

从函数返回错误的三种常见模式:

  1. 该函数是否返回int,并带有指示成功和失败的特定值

    例如,从EXIT_SUCCESS返回EXIT_FAILUREmain()是C标准建议报告整个过程成功或失败的方式。 (BSD变体已尝试标准化其他一些代码;如果您的系统具有<sysexits.h>标头,则可以使用这些标头。但是请注意,它们不是“标准”的,只是我们必须达成的最接近的协议进程可以报告错误代码。)

  2. 为错误保留一个特定的返回值,并使用全局或线程局部变量(通常为errno)来描述错误

    大多数标准C库函数都执行此操作,其中函数使用错误int返回-1,函数使用NULL返回指示错误的指针。
    < / p>

  3. 使用额外的参数指向错误指示器。

    这种方法在源自Fortran的代码和接口中很常见。错误指示符通常是可选的,如果调用者对结果是否有效不感兴趣,可以将其留在NULL上。

我自己的规则很简单:

  • 在编写低级库时,首选第二种方法。对于熟悉标准C库的人来说,这是一种熟悉的方法。

  • 使用第一种方法处理可恢复的错误。

    通常,我将其与第二个相结合,使用return 0;获得成功,并使用return errno;return errno = EINVAL;等获得错误。 (最后一个将EINVAL分配给errno,然后返回EINVAL。)

  • 在应通过多次操作保留错误状态或存在状态错误影响的结构时,使用第三种方法。


让我们看看这些方法在实践中有何不同。

一个非常普通的事情是将命令行参数解析为数字。让我们看一下将参数用作double进行某种计算的情况:

#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)有效,而char转换(%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+31/2/31:2:3甚至'1 2 3'"1 2 3"在命令行上指定3D向量(引号需要阻止外壳将其拆分为三个单独的参数)。它不会检查double的溢出情况,因此在输出中显示已解析的向量很重要,以便用户可以检测到他们的输入是否被错误地解析。

*中的星号%*[.,:/]表示转换结果未存储在任何地方,并且转换未计入返回值。[是转换说明符,“转换”列表中的所有字符,并以]字符结尾。[^是相反的,“转换”列表中的所有和所有字符 not 。 )

答案 1 :(得分:2)

不要使用指针返回带外错误代码。指针为NULL或有效(如果无效),通常在程序中堆栈溢出。 char *pointer = (char*)(uintptr_t)1;只是令人困惑,而if ((uintptr_t)pointer == 1) {.. }则难以维护。

返回intint在C标准库中很常见,会返回错误。通常,C库在发生错误时返回-1并设置errno-我通常编写的库代码返回的是错误代码的负值(即return -ENOMEMmalloc失败的情况下)。成功返回0并返回正值,以通知用户代码有关库中某些“状态”的信息。通过指针传递要设置的所有变量。看前fopen_s(观点:不要使用fopen_s,只看它)。

enum importantFunction_rets_e {
   IMPORTANT_FUNCTION_ERR_1 = -1,
   IMPORTANT_FUNCTION_ERR_2 = -2,
   IMPORTANT_FUNCTION_STATE_1 = 1,
   IMPORTANT_FUNCTION_STATE_2 = 2,
};

int importantFunction(char **pointer)
{
   assert(pointer != NULL);
   // or maybe
   if (pointer == NULL) return -EINVAL;

   int ret;
   ret = secondFunction(pointer);
   if (ret < 0) return ret;
   ret = thirdFunction(pointer);
   if (ret < 0) return ret; 
   return 0;
}

int secondFunction(char **pointer) {
  *pointer = malloc(sizeof(char) * 5);
   if (*pointer == NULL) {
      return IMPORTANT_FUNCTION_ERR_1;
   }
   memcpy(*pointer, "hey!", 5);
   return 0;
}

int main() {
    char *pointer;
    const int importantFunction_ret = importantFunction(&pointer);
    if (importantFunction_ret < 0) {
       if (importantFunction_ret == IMPORTANT_FUNCTION_ERR_1) {
          // handle err 1
       } else if (importantFunction_ret == IMPORTANT_FUNCTION_ERR_2) {
          // handle err 2
       } else {
          // hanlde other errors
       }
       return -1;
    }
    if (importantFunction_ret == IMPORTANT_FUNCTION_STATE_1) { 
         // handle state1
    } else if {importantFunction_ret == IMPORTANT_FUNCTION_STATE_2) {
        // handle state2
    } else {
        // handle other states 
        assert(0);
    }
}

如果您想探索C语言中的主题或错误处理方法,则可以沿新的(或旧的吗?)proposal实施某些与面向对象语言使用std::variant相同的方法。或std::expected或类似名称(观点:我现在确实反对该建议,它需要重新设计/重构,但这对于C而言将是一大进步)。

答案 2 :(得分:1)

在C中处理错误的一种常见方法是通过返回值。

说一个函数f成功后,返回一个指向字符串的指针。

char *f();

此类函数在失败时将返回NULL指针,您可以通过包含一些常见的头文件(例如<string.h>)来获得该指针。

现在说g是一个给定整数的函数,它计算某些内容并返回该运算的整数结果,但是该函数可能会失败(例如,该参数对于计算无效,谁知道...) 。那也许你想这样写

int g(int i, int *result);

在这里,i是计算内容的参数,而result是指向将用于存储结果的变量的指针。现在,为什么g返回输入int吗?好吧,它可能是bool中的<stdbool.h>,但是通常使用int ...返回值将用作布尔值,g将返回0失败则1成功。

您可以在第三个函数h

中像这样使用它们
int h(int i) {
    char *str = f();

    if (str == NULL) {
        printf("f failed !\n");
        return 0; // f failed
    }

    printf("%s\n", str);

    int result;
    if (!g(i, &result)) {
        printf("g failed !\n");
        return 0; // g failed
    } else {
        printf("result = %d\n", result);
    }

    return 1; // h success
}

答案 3 :(得分:0)

我意识到的一件事是,将致命错误渗透到堆栈中通常是不值得的。如果某件事以无法前进的方式失败,则在该处结束程序。我通常通过创建一个error_exit函数来处理它,该函数可以从任何地方调用:

void error_exit(int code, const char* message) {
  printf("Error %d: %s\nExiting!\n", code, message);
  cleanup();
  exit(code);
}

float* nested_function(int input, ...) {
  if (causes_hopeless_failure(input)) {
    error_exit(err_HOPELESS, "invalid input to nested_function");
  }
  //normal processing proceeds ...
  return valid_pointer;
}

int main() {
   float* vector = function_which_eventually_calls_nested_function();
   cleanup();
   return 0;
}

cleanup函数用于处理无法在程序退出时正确清理的资源。文件句柄和分配的缓冲区通常不属于此类。我通常将其用于需要撤消的系统配置更改。

答案 4 :(得分:0)

C具有带外错误代码处理功能,自那以来一直存在。

#include <errno.h>

int do_something(char* data) {
   if ( data == 0 ) {
      errno = ENODATA;
      return 0;
   }
   ... do stuff ...
}

...在呼叫者中...

int value = do_something( "one" );
if ( int errornum = errno ) {
   fprintf("error (%d) could not do something: %s", strerror( errornum ) );
   return; // or exit;
} 

如果希望链接错误

int value = do_something( "one" );
if ( int errornum = errno ) {
   fprintf("error (%d) could not do something: %s", strerror( errornum ) );
   errno = errornum;
   return; // or exit;
} 

请记住,几乎每个标准函数调用都会重置errno,因此您需要捕获它,然后有选择地执行任何操作后再设置它。

errno通常没有得到应有的重视的原因可能是因为首先要教太多的人带内错误报告(通过特殊的标记/值)。另外,需要花费更多的代码行来正确检查错误代码。也就是说,这是一个更好的解决方案,因为您不会在同一个变量中用数据和控制信息重载返回值。

已经设置了很多错误代码,奇怪的是您可以根据需要重复使用一个,或者选择一个足够接近的错误代码

1  EPERM Operation not permitted
2   ENOENT  No such file or directory
3   ESRCH   No such process
4   EINTR   Interrupted system call
5   EIO     I/O error
6   ENXIO   No such device or address
7   E2BIG   Argument list too long
8   ENOEXEC     Exec format error
9   EBADF   Bad file number
10  ECHILD  No child processes
11  EAGAIN  Try again
12  ENOMEM  Out of memory
13  EACCES  Permission denied
14  EFAULT  Bad address
15  ENOTBLK     Block device required
16  EBUSY   Device or resource busy
17  EEXIST  File exists
18  EXDEV   Cross-device link
19  ENODEV  No such device
20  ENOTDIR     Not a directory
21  EISDIR  Is a directory
22  EINVAL  Invalid argument
23  ENFILE  File table overflow
24  EMFILE  Too many open files
25  ENOTTY  Not a typewriter
26  ETXTBSY     Text file busy
27  EFBIG   File too large
28  ENOSPC  No space left on device
29  ESPIPE  Illegal seek
30  EROFS   Read-only file system
31  EMLINK  Too many links
32  EPIPE   Broken pipe
33  EDOM    Math argument out of domain of func
34  ERANGE  Math result not representable
35  EDEADLK     Resource deadlock would occur
36  ENAMETOOLONG    File name too long
37  ENOLCK  No record locks available
38  ENOSYS  Function not implemented
39  ENOTEMPTY   Directory not empty
40  ELOOP   Too many symbolic links encountered
42  ENOMSG  No message of desired type
43  EIDRM   Identifier removed
44  ECHRNG  Channel number out of range
45  EL2NSYNC    Level 2 not synchronized
46  EL3HLT  Level 3 halted
47  EL3RST  Level 3 reset
48  ELNRNG  Link number out of range
49  EUNATCH     Protocol driver not attached
50  ENOCSI  No CSI structure available
51  EL2HLT  Level 2 halted
52  EBADE   Invalid exchange
53  EBADR   Invalid request descriptor
54  EXFULL  Exchange full
55  ENOANO  No anode
56  EBADRQC     Invalid request code
57  EBADSLT     Invalid slot
59  EBFONT  Bad font file format
60  ENOSTR  Device not a stream
61  ENODATA     No data available
62  ETIME   Timer expired
63  ENOSR   Out of streams resources
64  ENONET  Machine is not on the network
65  ENOPKG  Package not installed
66  EREMOTE     Object is remote
67  ENOLINK     Link has been severed
68  EADV    Advertise error
69  ESRMNT  Srmount error
70  ECOMM   Communication error on send
71  EPROTO  Protocol error
72  EMULTIHOP   Multihop attempted
73  EDOTDOT     RFS specific error
74  EBADMSG     Not a data message
75  EOVERFLOW   Value too large for defined data type
76  ENOTUNIQ    Name not unique on network
77  EBADFD  File descriptor in bad state
78  EREMCHG     Remote address changed
79  ELIBACC     Can not access a needed shared library
80  ELIBBAD     Accessing a corrupted shared library
81  ELIBSCN     .lib section in a.out corrupted
82  ELIBMAX     Attempting to link in too many shared libraries
83  ELIBEXEC    Cannot exec a shared library directly
84  EILSEQ  Illegal byte sequence
85  ERESTART    Interrupted system call should be restarted
86  ESTRPIPE    Streams pipe error
87  EUSERS  Too many users
88  ENOTSOCK    Socket operation on non-socket
89  EDESTADDRREQ    Destination address required
90  EMSGSIZE    Message too long
91  EPROTOTYPE  Protocol wrong type for socket
92  ENOPROTOOPT     Protocol not available
93  EPROTONOSUPPORT     Protocol not supported
94  ESOCKTNOSUPPORT     Socket type not supported
95  EOPNOTSUPP  Operation not supported on transport endpoint
96  EPFNOSUPPORT    Protocol family not supported
97  EAFNOSUPPORT    Address family not supported by protocol
98  EADDRINUSE  Address already in use
99  EADDRNOTAVAIL   Cannot assign requested address
100     ENETDOWN    Network is down
101     ENETUNREACH     Network is unreachable
102     ENETRESET   Network dropped connection because of reset
103     ECONNABORTED    Software caused connection abort
104     ECONNRESET  Connection reset by peer
105     ENOBUFS     No buffer space available
106     EISCONN     Transport endpoint is already connected
107     ENOTCONN    Transport endpoint is not connected
108     ESHUTDOWN   Cannot send after transport endpoint shutdown
109     ETOOMANYREFS    Too many references: cannot splice
110     ETIMEDOUT   Connection timed out
111     ECONNREFUSED    Connection refused
112     EHOSTDOWN   Host is down
113     EHOSTUNREACH    No route to host
114     EALREADY    Operation already in progress
115     EINPROGRESS     Operation now in progress
116     ESTALE  Stale NFS file handle
117     EUCLEAN     Structure needs cleaning
118     ENOTNAM     Not a XENIX named type file
119     ENAVAIL     No XENIX semaphores available
120     EISNAM  Is a named type file
121     EREMOTEIO   Remote I/O error
122     EDQUOT  Quota exceeded
123     ENOMEDIUM   No medium found
124     EMEDIUMTYPE     Wrong medium type
125     ECANCELED   Operation Canceled
126     ENOKEY  Required key not available
127     EKEYEXPIRED     Key has expired
128     EKEYREVOKED     Key has been revoked
129     EKEYREJECTED    Key was rejected by service
130     EOWNERDEAD  Owner died
131     ENOTRECOVERABLE     State not recoverable