C : 你如何模拟“异常”?

Posted

技术标签:

【中文标题】C : 你如何模拟“异常”?【英文标题】:C : How do you simulate an 'exception'? 【发布时间】:2010-11-16 04:59:02 【问题描述】:

我来自 C# 背景,但我目前正在学习 C。在 C# 中,当人们想要发出错误已发生的信号时,您会抛出异常。但是你用 C 做什么呢?

例如,假设您有一个包含 pushpop 函数的堆栈。在 pop 期间表明堆栈为空的最佳方式是什么?你从那个函数返回什么?

double pop(void)

    if(sp > 0)
        return val[--sp];
    else 
        printf("error: stack empty\n");
        return 0.0;
    

K&R 的第 77 页示例(上面的代码)返回 0.0。但是如果用户之前在栈中推送了一个0.0,你怎么知道栈是空的还是返回了正确的值呢?

【问题讨论】:

【参考方案1】:

一种方法是在堆栈为空时指定 pop() 具有未定义的行为。然后你必须提供一个 is_empty() 函数,可以调用它来检查堆栈。

另一种方法是使用 C++,它确实有例外 :-)

【讨论】:

对于这种特殊情况更有用的是,C++ 在库中有一个堆栈:-) 但是 C++ std::stack pop 函数实际上并没有做 OP 想要的。 是的,并且理解为什么会增加 OP 的 C++ 教育,这是问题的主要目的:-) 无论如何,将一个电话分别包装到 top()pop(),返回一份副本,给出的最终结果与获取 OP 所拥有的内容并应用您所说的需要 empty() 函数的内容相同。 IYSWIM。【参考方案2】:

C 中的类异常行为是通过setjmp/longjmp 完成的。但是,您真正想要的是错误代码。如果所有值都可能返回,那么您可能希望接收一个 out 参数作为指针,并使用它来返回值,如下所示:

int pop(double* outval)

        if(outval == 0) return -1;
        if(sp > 0)
                *outval = val[--sp];
        else 
                printf("error: stack empty\n");
                return -1;
        
        return 0;

显然不理想,但这就是 C 语言的局限性。

另外,如果你走这条路,你可能想为你的错误代码定义符号常量(或使用一些the standard ones),以便用户可以区分“堆栈空”和“你给了我一个空值”指针,笨蛋”。

【讨论】:

我有点不同意,因为即使我明白你的意思,我也不会让来自 java/c# 的人假设 setjmp/longjmp 在任何方面都是“解决方案”到“在哪里”我的例外?' Jonke 是对的——setjmp/longjmp 只模拟了抛出异常的一小部分。面对由此产生的怪异控制流,您需要编写异常安全代码的能力,为此您需要析构函数(或 try/finally)。没有它,唯一可管理的方法是错误代码返回值。 我想是的。真正的问题是“在 C 中,如果所有可能的返回值都有效,函数如何指示错误?”。发帖人只是假设答案是某种形式的异常,因为他来自 C#。 @Dreas Grech:嗯? C# 中肯定有例外……我同意这里的其他一些观点;仅仅因为你知道如何用一种语言做某事并不意味着它在任何地方都是这样做的。 C 不是 Java;使用返回码,即 C 方式。 不不抱歉,我的意思是 C。我之前评论中的“C#”是一个错字【参考方案3】:

你可以返回一个指向 double 的指针:

非 NULL -> 有效 NULL -> 无效

【讨论】:

没有cmets的downvotes是没有意义的,请解释你的downvotes 我不是反对者,但我想知道指针的后备存储来自哪里。如果它是弹出的元素,那么调用者必须在推送新值之前取消引用它。如果它是一个单独的static double,那么调用者必须在下一次调用 pop 之前取消引用。两者都会给调用代码带来很多麻烦。 我也没有投反对票,但我也有同样的担忧。我认为这是一种有效的方法,但您需要更改函数的工作方式并存储数据才能做到这一点。【参考方案4】:

你有几个选择:

1) 魔术错误值。由于您描述的原因,并不总是足够好。我想在这种情况下理论上你可以返回一个 NaN,但我不推荐它。

2) 定义栈为空时弹出无效。然后你的代码要么只是假设它是非空的(如果它是未定义的),或者断言。

3) 更改函数的签名,以便您可以指示成功或失败:

int pop(double *dptr)

    if(sp > 0) 
            *dptr = val[--sp];
            return 0;
     else 
            return 1;
    

将其记录为“如果成功,则返回0并将值写入dptr指向的位置。失败时,返回非零值。”

您可以选择使用返回值或errno 来指示失败的原因,尽管对于这个特定示例只有一个原因。

4) 通过指针将“异常”对象传递给每个函数,并在失败时向其写入值。调用者然后根据他们如何使用返回值来检查它。这很像使用“errno”,但它不是线程范围的值。

5) 正如其他人所说,使用 setjmp/longjmp 实现异常。这是可行的,但需要在任何地方传递一个额外的参数(longjmp 的目标在失败时执行),或者将其隐藏在全局变量中。它还使典型的 C 风格资源处理成为一场噩梦,因为如果你持有一个你负责释放的资源,你就不能调用任何可能跳出堆栈级别的东西。

【讨论】:

【参考方案5】:

您可以在 longjmp/setjmp 之上构建一个异常系统:Exceptions in C with Longjmp and Setjmp。它实际上工作得很好,这篇文章也很好读。如果您使用链接文章中的异常系统,您的代码如下所示:

  TRY 
    ...
    THROW(MY_EXCEPTION);
    /* Unreachable */
   CATCH(MY_EXCEPTION) 
    ...
   CATCH(OTHER_EXCEPTION) 
    ...
   FINALLY 
    ...
  

使用一些宏可以做的事情真是太棒了,对吧?如果您还不知道宏的作用,那么要弄清楚到底发生了什么是多么困难,这同样令人惊讶。

longjmp/setjmp 是可移植的:C89、C99 和 POSIX.1-2001 指定 setjmp()

但是请注意,与 C# 或 C++ 中的“真实”异常相比,以这种方式实现的异常仍有一些限制。一个主要问题是只有你的代码才能与这个异常系统兼容。由于 C 语言中的异常没有既定标准,因此系统和第三方库无法与您自己开发的异常系统进行最佳互操作。不过,这有时会成为一个有用的技巧。

我不建议在严肃的代码中使用它,除了你自己之外的程序员应该使用这些代码。如果你不知道到底发生了什么,这太容易把自己打死了。线程、资源管理和信号处理是非玩具程序在尝试使用 longjmp“异常”时会遇到的问题。

【讨论】:

在异常变得广泛可用之前,我实际上在 C++ 中构建了类似的东西。我什至通过自己的堆栈展开形式来实现。幸运的是,在我们将它用于生产代码之前,我已经意识到了这一点。 @Neil:我喜欢那句话的第二部分 :-) “幸运的是,我醒悟了”。恭喜。 Symbian 做了与您相同的事情,直到您意识到这一点,然后它们就发货了。 10 多年后,他们仍然到处都有 NewLC... 我猜当从更深的函数调用时,THROW 语句将不起作用。在函数中可以简单地使用 goto。【参考方案6】:

没有等同于直接 C 中的异常。您必须设计函数签名以返回错误信息,如果这是您想要的。

C 中可用的机制是:

使用 setjmp/longjmp 的非本地 goto 信号

但是,这些都没有与 C#(或 C++)异常相似的语义。

【讨论】:

【参考方案7】:

在这种情况下,您通常会执行以下操作之一

留给调用者。例如由调用者知道 pop() 是否安全(例如,在弹出堆栈之前调用 stack->is_empty() 函数),如果调用者搞砸了,那是他的错,祝你好运。 通过输出参数或返回值指示错误。

例如你要么做

double pop(int *error)

  if(sp > 0) 
      return val[--sp];
      *error = 0;
   else 
     *error = 1;
      printf("error: stack empty\n");
      return 0.0;
  

int pop(double *d)

   if(sp > 0)  
       *d = val[--sp];
       return 0;
    else 
     return 1;

   

【讨论】:

【参考方案8】:

这里已经有一些很好的答案,只是想提一下接近“异常”的东西,可以通过使用宏来完成,就像在很棒的 MinUnit 中所做的那样(这只返回“异常”到调用者函数)。

【讨论】:

【参考方案9】:

1) 您返回一个标志值以表明它失败,或者您使用 TryGet 语法,其中返回是成功的布尔值,而值通过输出参数传递。

2) 如果这是在 Windows 下,则存在操作系统级别的纯 C 异常形式,称为结构化异常处理,使用类似“_try”的语法。我提到了它,但我不建议在这种情况下使用它。

【讨论】:

【参考方案10】:

这实际上是一个完美的例子,试图用魔法值重载返回类型和简单的有问题的界面设计。

我可以用来消除示例中的歧义(因此需要“类似异常的行为”)的一个解决方案是定义一个适当的返回类型:

struct stack
    double* pData;
    uint32  size;
;

struct popRC
    double value;
    uint32 size_before_pop;
;

popRC pop(struct stack* pS)
    popRC rc;
    rc.size=pS->size;
    if(rc.size)
        --pS->size;
        rc.value=pS->pData[pS->size];
    
    return rc;

用法当然是:

popRC rc = pop(&stack);
if(rc.size_before_pop!=0)
    ....use rc.value

这种情况经常发生,但在 C++ 中,为了避免这种歧义,通常只返回一个

std::pair<something,bool>

bool 是成功指标的地方 - 看看一些:

std::set<...>::insert
std::map<...>::insert

或者在接口中添加一个double*并返回一个(n UNOVERLOADED!)返回码,比如一个表示成功的枚举。

当然不必返回结构popRC 中的大小。本来可以

enumFAIL,SUCCESS;

但由于尺寸可能会作为对 pop'er 的有用提示,您不妨使用它。

顺便说一句,我非常同意结构堆栈接口应该有

int empty(struct stack* pS)
    return (pS->size == 0) ? 1 : 0;

【讨论】:

【参考方案11】:

setjmplongjmp 和宏。它已经完成了很多次——我所知道的最古老的实现是由 Eric Roberts 和 Mark vanderVoorde 实现的——但我目前使用的是 Dave Hanson 的C Interfaces and Implementations 的一部分,并且从普林斯顿免费获得。

【讨论】:

【参考方案12】:

一些还没有人提到的东西,虽然它很丑:

int ok=0;

do

   /* Do stuff here */

   /* If there is an error */
   break;

   /* If we got to the end without an error */
   ok=1;

 while(0);

if (ok == 0)

   printf("Fail.\n");

else

   printf("Ok.\n");

【讨论】:

以上是关于C : 你如何模拟“异常”?的主要内容,如果未能解决你的问题,请参考以下文章

你如何在 GoLang 的模拟中模拟错误返回值?

如何模拟 void 方法来引发异常?

如何使用 Mockery 在模拟方法的第 N 次调用中引发异常

如何修复 Android 模拟器中的“无法连接到相机服务”异常

电脑成功安装好Android4.0模拟器后,如何在模拟器中安装软件?

两种异常(CPU异常用户模拟异常)的收集